jiff 0.1.15

A date-time library that encourages you to jump into the pit of success. This library is heavily inspired by the Temporal project.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
use crate::{
    civil::{Date, Weekday},
    error::Error,
    util::{
        rangeint::RInto,
        t::{self, ISOWeek, ISOYear, C},
    },
};

/// A type representing an [ISO 8601 week date].
///
/// The ISO 8601 week date scheme devises a calendar where days are identified
/// by their year, week number and weekday. All years have either precisely
/// 52 or 53 weeks.
///
/// The first week of an ISO 8601 year corresponds to the week containing the
/// first Thursday of the year. For this reason, an ISO 8601 week year can be
/// mismatched with the day's corresponding Gregorian year. For example, the
/// ISO 8601 week date for `1995-01-01` is `1994-W52-7` (with `7` corresponding
/// to Sunday).
///
/// ISO 8601 also considers Monday to be the start of the week, and uses
/// a 1-based numbering system. That is, Monday corresponds to `1` while
/// Sunday corresponds to `7` and is the last day of the week. Weekdays are
/// encapsulated by the [`Weekday`] type, which provides routines for easily
/// converting between different schemes (such as weeks where Sunday is the
/// beginning).
///
/// [ISO 8601 week date]: https://en.wikipedia.org/wiki/ISO_week_date
///
/// # Use case
///
/// Some domains use this method of timekeeping. Otherwise, unless you
/// specifically want a week oriented calendar, it's likely that you'll never
/// need to care about this type.
///
/// # Default value
///
/// For convenience, this type implements the `Default` trait. Its default
/// value is the first day of the zeroth year. i.e., `0000-W1-1`.
///
/// # Example: sample dates
///
/// This example shows a couple ISO 8601 week dates and their corresponding
/// Gregorian equivalents:
///
/// ```
/// use jiff::civil::{ISOWeekDate, Weekday, date};
///
/// let d = date(2019, 12, 30);
/// let weekdate = ISOWeekDate::new(2020, 1, Weekday::Monday).unwrap();
/// assert_eq!(d.to_iso_week_date(), weekdate);
///
/// let d = date(2024, 3, 9);
/// let weekdate = ISOWeekDate::new(2024, 10, Weekday::Saturday).unwrap();
/// assert_eq!(d.to_iso_week_date(), weekdate);
/// ```
///
/// # Example: overlapping leap and long years
///
/// A "long" ISO 8601 week year is a year with 53 weeks. That is, it is a year
/// that includes a leap week. This example shows all years in the 20th
/// century that are both Gregorian leap years and long years.
///
/// ```
/// use jiff::civil::date;
///
/// let mut overlapping = vec![];
/// for year in 1900..=1999 {
///     let date = date(year, 1, 1);
///     if date.in_leap_year() && date.to_iso_week_date().in_long_year() {
///         overlapping.push(year);
///     }
/// }
/// assert_eq!(overlapping, vec![
///     1904, 1908, 1920, 1932, 1936, 1948, 1960, 1964, 1976, 1988, 1992,
/// ]);
/// ```
#[derive(Clone, Copy, Hash)]
pub struct ISOWeekDate {
    year: ISOYear,
    week: ISOWeek,
    weekday: Weekday,
}

impl ISOWeekDate {
    /// The maximum representable ISO week date.
    ///
    /// The maximum corresponds to the ISO week date of the maximum [`Date`]
    /// value. That is, `-9999-01-01`.
    pub const MIN: ISOWeekDate = ISOWeekDate {
        year: ISOYear::new_unchecked(-9999),
        week: ISOWeek::new_unchecked(1),
        weekday: Weekday::Monday,
    };

    /// The minimum representable ISO week date.
    ///
    /// The minimum corresponds to the ISO week date of the minimum [`Date`]
    /// value. That is, `9999-12-31`.
    pub const MAX: ISOWeekDate = ISOWeekDate {
        year: ISOYear::new_unchecked(9999),
        week: ISOWeek::new_unchecked(52),
        weekday: Weekday::Friday,
    };

    /// The first day of the zeroth year.
    ///
    /// This is guaranteed to be equivalent to `ISOWeekDate::default()`. Note
    /// that this is not equivalent to `Date::default()`.
    ///
    /// # Example
    ///
    /// ```
    /// use jiff::civil::{ISOWeekDate, date};
    ///
    /// assert_eq!(ISOWeekDate::ZERO, ISOWeekDate::default());
    /// // The first day of the 0th year in the ISO week calendar is actually
    /// // the third day of the 0th year in the proleptic Gregorian calendar!
    /// assert_eq!(ISOWeekDate::default().to_date(), date(0, 1, 3));
    /// ```
    pub const ZERO: ISOWeekDate = ISOWeekDate {
        year: ISOYear::new_unchecked(0),
        week: ISOWeek::new_unchecked(1),
        weekday: Weekday::Monday,
    };

    /// Create a new ISO week date from it constituent parts.
    ///
    /// If the given values are out of range (based on what is representable
    /// as a [`Date`]), then this returns an error. This will also return an
    /// error if a leap week is given (week number `53`) for a year that does
    /// not contain a leap week.
    ///
    /// # Example
    ///
    /// This example shows some the boundary conditions involving minimum
    /// and maximum dates:
    ///
    /// ```
    /// use jiff::civil::{ISOWeekDate, Weekday, date};
    ///
    /// // The year 1949 does not contain a leap week.
    /// assert!(ISOWeekDate::new(1949, 53, Weekday::Monday).is_err());
    ///
    /// // Examples of dates at or exceeding the maximum.
    /// let max = ISOWeekDate::new(9999, 52, Weekday::Friday).unwrap();
    /// assert_eq!(max, ISOWeekDate::MAX);
    /// assert_eq!(max.to_date(), date(9999, 12, 31));
    /// assert!(ISOWeekDate::new(9999, 52, Weekday::Saturday).is_err());
    /// assert!(ISOWeekDate::new(9999, 53, Weekday::Monday).is_err());
    ///
    /// // Examples of dates at or exceeding the minimum.
    /// let min = ISOWeekDate::new(-9999, 1, Weekday::Monday).unwrap();
    /// assert_eq!(min, ISOWeekDate::MIN);
    /// assert_eq!(min.to_date(), date(-9999, 1, 1));
    /// assert!(ISOWeekDate::new(-10000, 52, Weekday::Sunday).is_err());
    /// ```
    #[inline]
    pub fn new(
        year: i16,
        week: i8,
        weekday: Weekday,
    ) -> Result<ISOWeekDate, Error> {
        let year = ISOYear::try_new("year", year)?;
        let week = ISOWeek::try_new("week", week)?;
        ISOWeekDate::new_ranged(year, week, weekday)
    }

    /// Converts a Gregorian date to an ISO week date.
    ///
    /// The minimum and maximum allowed values of an ISO week date are
    /// set based on the minimum and maximum values of a `Date`. Therefore,
    /// converting to and from `Date` values is non-lossy and infallible.
    ///
    /// This routine is equivalent to [`Date::to_iso_week_date`]. This
    /// routine is also available via a `From<Date>` trait implementation for
    /// `ISOWeekDate`.
    ///
    /// # Example
    ///
    /// ```
    /// use jiff::civil::{ISOWeekDate, Weekday, date};
    ///
    /// let weekdate = ISOWeekDate::from_date(date(1948, 2, 10));
    /// assert_eq!(
    ///     weekdate,
    ///     ISOWeekDate::new(1948, 7, Weekday::Tuesday).unwrap(),
    /// );
    /// ```
    #[inline]
    pub fn from_date(date: Date) -> ISOWeekDate {
        date.to_iso_week_date()
    }

    // N.B. I tried defining a `ISOWeekDate::constant` for defining ISO week
    // dates as constants, but it was too annoying to do. We could do it if
    // there was a compelling reason for it though.

    /// Returns the year component of this ISO 8601 week date.
    ///
    /// The value returned is guaranteed to be in the range `-9999..=9999`.
    ///
    /// # Example
    ///
    /// ```
    /// use jiff::civil::date;
    ///
    /// let weekdate = date(2019, 12, 30).to_iso_week_date();
    /// assert_eq!(weekdate.year(), 2020);
    /// ```
    #[inline]
    pub fn year(self) -> i16 {
        self.year_ranged().get()
    }

    /// Returns the week component of this ISO 8601 week date.
    ///
    /// The value returned is guaranteed to be in the range `1..=53`. A
    /// value of `53` can only occur for "long" years. That is, years
    /// with a leap week. This occurs precisely in cases for which
    /// [`ISOWeekDate::in_long_year`] returns `true`.
    ///
    /// # Example
    ///
    /// ```
    /// use jiff::civil::date;
    ///
    /// let weekdate = date(2019, 12, 30).to_iso_week_date();
    /// assert_eq!(weekdate.year(), 2020);
    /// assert_eq!(weekdate.week(), 1);
    ///
    /// let weekdate = date(1948, 12, 31).to_iso_week_date();
    /// assert_eq!(weekdate.year(), 1948);
    /// assert_eq!(weekdate.week(), 53);
    /// ```
    #[inline]
    pub fn week(self) -> i8 {
        self.week_ranged().get()
    }

    /// Returns the day component of this ISO 8601 week date.
    ///
    /// One can use methods on `Weekday` such as
    /// [`Weekday::to_sunday_zero_offset`] to convert the weekday to a number.
    ///
    /// # Example
    ///
    /// ```
    /// use jiff::civil::{date, Weekday};
    ///
    /// let weekdate = date(1948, 12, 31).to_iso_week_date();
    /// assert_eq!(weekdate.year(), 1948);
    /// assert_eq!(weekdate.week(), 53);
    /// assert_eq!(weekdate.weekday(), Weekday::Friday);
    /// ```
    #[inline]
    pub fn weekday(self) -> Weekday {
        self.weekday
    }

    /// Returns true if and only if the year of this week date is a "long"
    /// year.
    ///
    /// A long year is one that contains precisely 53 weeks. All other years
    /// contain precisely 52 weeks.
    ///
    /// # Example
    ///
    /// ```
    /// use jiff::civil::{ISOWeekDate, Weekday};
    ///
    /// let weekdate = ISOWeekDate::new(1948, 7, Weekday::Monday).unwrap();
    /// assert!(weekdate.in_long_year());
    /// let weekdate = ISOWeekDate::new(1949, 7, Weekday::Monday).unwrap();
    /// assert!(!weekdate.in_long_year());
    /// ```
    #[inline]
    pub fn in_long_year(self) -> bool {
        is_long_year(self.year_ranged())
    }

    /// Converts this ISO week date to a Gregorian [`Date`].
    ///
    /// The minimum and maximum allowed values of an ISO week date are
    /// set based on the minimum and maximum values of a `Date`. Therefore,
    /// converting to and from `Date` values is non-lossy and infallible.
    ///
    /// This routine is equivalent to [`Date::from_iso_week_date`].
    ///
    /// # Example
    ///
    /// ```
    /// use jiff::civil::{ISOWeekDate, Weekday, date};
    ///
    /// let weekdate = ISOWeekDate::new(1948, 7, Weekday::Tuesday).unwrap();
    /// assert_eq!(weekdate.to_date(), date(1948, 2, 10));
    /// ```
    #[inline]
    pub fn to_date(self) -> Date {
        Date::from_iso_week_date(self)
    }
}

impl ISOWeekDate {
    /// Creates a new ISO week date from ranged values.
    ///
    /// While the ranged values given eliminate some error cases, not all
    /// combinations of year/week/weekday values are valid ISO week dates
    /// supported by this crate. For example, a week of `53` for short years,
    /// or more niche, a week date that would be bigger than what is supported
    /// by our `Date` type.
    #[inline]
    pub(crate) fn new_ranged(
        year: impl RInto<ISOYear>,
        week: impl RInto<ISOWeek>,
        weekday: Weekday,
    ) -> Result<ISOWeekDate, Error> {
        let year = year.rinto();
        let week = week.rinto();
        // All combinations of years, weeks and weekdays allowed by our
        // range types are valid ISO week dates with one exception: a week
        // number of 53 is only valid for "long" years. Or years with an ISO
        // leap week. It turns out this only happens when the last day of the
        // year is a Thursday.
        //
        // Note that if the ranges in this crate are changed, this could be
        // a little trickier if the range of ISOYear is different from Year.
        debug_assert_eq!(t::Year::MIN, ISOYear::MIN);
        debug_assert_eq!(t::Year::MAX, ISOYear::MAX);
        if week == 53 && !is_long_year(year) {
            return Err(Error::specific("ISO week number", week));
        }
        // And also, the maximum Date constrains what we can utter with
        // ISOWeekDate so that we can preserve infallible conversions between
        // them. So since 9999-12-31 maps to 9999 W52 Friday, it follows that
        // Saturday and Sunday are not allowed. So reject them.
        //
        // We don't need to worry about the minimum because the minimum date
        // (-9999-01-01) corresponds also to the minimum possible combination
        // of an ISO week date's fields: -9999 W01 Monday. Nice.
        if year == ISOYear::MAX_SELF
            && week == 52
            && weekday.to_monday_zero_offset()
                > Weekday::Friday.to_monday_zero_offset()
        {
            return Err(Error::signed(
                "weekday",
                weekday.to_monday_zero_offset(),
                Weekday::Monday.to_monday_one_offset(),
                Weekday::Friday.to_monday_one_offset(),
            ));
        }
        Ok(ISOWeekDate { year, week, weekday })
    }

    /// Like `ISOWeekDate::new_ranged`, but constrains out-of-bounds values
    /// to their closest valid equivalent.
    ///
    /// For example, given 9999 W52 Saturday, this will return 9999 W52 Friday.
    #[cfg(test)]
    #[inline]
    pub(crate) fn new_ranged_constrain(
        year: impl RInto<ISOYear>,
        week: impl RInto<ISOWeek>,
        mut weekday: Weekday,
    ) -> ISOWeekDate {
        let year = year.rinto();
        let mut week = week.rinto();
        debug_assert_eq!(t::Year::MIN, ISOYear::MIN);
        debug_assert_eq!(t::Year::MAX, ISOYear::MAX);
        if week == 53 && !is_long_year(year) {
            week = ISOWeek::new(52).unwrap();
        }
        if year == ISOYear::MAX_SELF
            && week == 52
            && weekday.to_monday_zero_offset()
                > Weekday::Friday.to_monday_zero_offset()
        {
            weekday = Weekday::Friday;
        }
        ISOWeekDate { year, week, weekday }
    }

    #[inline]
    pub(crate) fn year_ranged(self) -> ISOYear {
        self.year
    }

    #[inline]
    pub(crate) fn week_ranged(self) -> ISOWeek {
        self.week
    }
}

impl Default for ISOWeekDate {
    fn default() -> ISOWeekDate {
        ISOWeekDate::ZERO
    }
}

impl core::fmt::Debug for ISOWeekDate {
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        f.debug_struct("ISOWeekDate")
            .field("year", &self.year_ranged().debug())
            .field("week", &self.week_ranged().debug())
            .field("weekday", &self.weekday)
            .finish()
    }
}

impl Eq for ISOWeekDate {}

impl PartialEq for ISOWeekDate {
    #[inline]
    fn eq(&self, other: &ISOWeekDate) -> bool {
        // We roll our own so that we can call 'get' on our ranged integers
        // in order to provoke panics for bugs in dealing with boundary
        // conditions.
        self.weekday == other.weekday
            && self.week.get() == other.week.get()
            && self.year.get() == other.year.get()
    }
}

impl Ord for ISOWeekDate {
    #[inline]
    fn cmp(&self, other: &ISOWeekDate) -> core::cmp::Ordering {
        (self.year.get(), self.week.get(), self.weekday.to_monday_one_offset())
            .cmp(&(
                other.year.get(),
                other.week.get(),
                other.weekday.to_monday_one_offset(),
            ))
    }
}

impl PartialOrd for ISOWeekDate {
    #[inline]
    fn partial_cmp(&self, other: &ISOWeekDate) -> Option<core::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl From<Date> for ISOWeekDate {
    #[inline]
    fn from(date: Date) -> ISOWeekDate {
        ISOWeekDate::from_date(date)
    }
}

#[cfg(test)]
impl quickcheck::Arbitrary for ISOWeekDate {
    fn arbitrary(g: &mut quickcheck::Gen) -> ISOWeekDate {
        let year = ISOYear::arbitrary(g);
        let week = ISOWeek::arbitrary(g);
        let weekday = Weekday::arbitrary(g);
        ISOWeekDate::new_ranged_constrain(year, week, weekday)
    }

    fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = ISOWeekDate>> {
        alloc::boxed::Box::new(
            (self.year_ranged(), self.week_ranged(), self.weekday())
                .shrink()
                .map(|(year, week, weekday)| {
                    ISOWeekDate::new_ranged_constrain(year, week, weekday)
                }),
        )
    }
}

/// Returns true if the given ISO year is a "long" year or not.
///
/// A "long" year is a year with 53 weeks. Otherwise, it's a "short" year
/// with 52 weeks.
fn is_long_year(year: ISOYear) -> bool {
    // Inspired by: https://en.wikipedia.org/wiki/ISO_week_date#Weeks_per_year
    let last = Date::new_ranged(year, C(12), C(31))
        .expect("last day of year is always valid");
    let weekday = last.weekday();
    weekday == Weekday::Thursday
        || (last.in_leap_year() && weekday == Weekday::Friday)
}

#[cfg(test)]
mod tests {
    use super::*;

    quickcheck::quickcheck! {
        fn prop_all_long_years_have_53rd_week(year: ISOYear) -> bool {
            !is_long_year(year)
                || ISOWeekDate::new(year.get(), 53, Weekday::Sunday).is_ok()
        }

        fn prop_prev_day_is_less(wd: ISOWeekDate) -> quickcheck::TestResult {
            use crate::ToSpan;

            if wd == ISOWeekDate::MIN {
                return quickcheck::TestResult::discard();
            }
            let prev_date = wd.to_date().checked_add(-1.days()).unwrap();
            quickcheck::TestResult::from_bool(prev_date.to_iso_week_date() < wd)
        }

        fn prop_next_day_is_greater(wd: ISOWeekDate) -> quickcheck::TestResult {
            use crate::ToSpan;

            if wd == ISOWeekDate::MAX {
                return quickcheck::TestResult::discard();
            }
            let next_date = wd.to_date().checked_add(1.days()).unwrap();
            quickcheck::TestResult::from_bool(wd < next_date.to_iso_week_date())
        }
    }
}