jiff/civil/iso_week_date.rs
1use crate::{
2 civil::{Date, DateTime, Weekday},
3 error::{civil::Error as E, Error},
4 fmt::temporal::{DEFAULT_DATETIME_PARSER, DEFAULT_DATETIME_PRINTER},
5 util::{
6 rangeint::RInto,
7 t::{self, ISOWeek, ISOYear, C},
8 },
9 Zoned,
10};
11
12/// A type representing an [ISO 8601 week date].
13///
14/// The ISO 8601 week date scheme devises a calendar where days are identified
15/// by their year, week number and weekday. All years have either precisely
16/// 52 or 53 weeks.
17///
18/// The first week of an ISO 8601 year corresponds to the week containing the
19/// first Thursday of the year. For this reason, an ISO 8601 week year can be
20/// mismatched with the day's corresponding Gregorian year. For example, the
21/// ISO 8601 week date for `1995-01-01` is `1994-W52-7` (with `7` corresponding
22/// to Sunday).
23///
24/// ISO 8601 also considers Monday to be the start of the week, and uses
25/// a 1-based numbering system. That is, Monday corresponds to `1` while
26/// Sunday corresponds to `7` and is the last day of the week. Weekdays are
27/// encapsulated by the [`Weekday`] type, which provides routines for easily
28/// converting between different schemes (such as weeks where Sunday is the
29/// beginning).
30///
31/// [ISO 8601 week date]: https://en.wikipedia.org/wiki/ISO_week_date
32///
33/// # Use case
34///
35/// Some domains use this method of timekeeping. Otherwise, unless you
36/// specifically want a week oriented calendar, it's likely that you'll never
37/// need to care about this type.
38///
39/// # Parsing and printing
40///
41/// The `ISOWeekDate` type provides convenient trait implementations of
42/// [`std::str::FromStr`] and [`std::fmt::Display`]. These use the format
43/// specified by ISO 8601 for week dates:
44///
45/// ```
46/// use jiff::civil::ISOWeekDate;
47///
48/// let week_date: ISOWeekDate = "2024-W24-7".parse()?;
49/// assert_eq!(week_date.to_string(), "2024-W24-7");
50/// assert_eq!(week_date.date().to_string(), "2024-06-16");
51///
52/// # Ok::<(), Box<dyn std::error::Error>>(())
53/// ```
54///
55/// ISO 8601 allows the `-` separator to be absent:
56///
57/// ```
58/// use jiff::civil::ISOWeekDate;
59///
60/// let week_date: ISOWeekDate = "2024W241".parse()?;
61/// assert_eq!(week_date.to_string(), "2024-W24-1");
62/// assert_eq!(week_date.date().to_string(), "2024-06-10");
63///
64/// // But you cannot mix and match. Either `-` separates
65/// // both the year and week, or neither.
66/// assert!("2024W24-1".parse::<ISOWeekDate>().is_err());
67/// assert!("2024-W241".parse::<ISOWeekDate>().is_err());
68///
69/// # Ok::<(), Box<dyn std::error::Error>>(())
70/// ```
71///
72/// And the `W` may also be lowercase:
73///
74/// ```
75/// use jiff::civil::ISOWeekDate;
76///
77/// let week_date: ISOWeekDate = "2024-w24-2".parse()?;
78/// assert_eq!(week_date.to_string(), "2024-W24-2");
79/// assert_eq!(week_date.date().to_string(), "2024-06-11");
80///
81/// # Ok::<(), Box<dyn std::error::Error>>(())
82/// ```
83///
84/// # Default value
85///
86/// For convenience, this type implements the `Default` trait. Its default
87/// value is the first day of the zeroth year. i.e., `0000-W1-1`.
88///
89/// # Example: sample dates
90///
91/// This example shows a couple ISO 8601 week dates and their corresponding
92/// Gregorian equivalents:
93///
94/// ```
95/// use jiff::civil::{ISOWeekDate, Weekday, date};
96///
97/// let d = date(2019, 12, 30);
98/// let weekdate = ISOWeekDate::new(2020, 1, Weekday::Monday).unwrap();
99/// assert_eq!(d.iso_week_date(), weekdate);
100///
101/// let d = date(2024, 3, 9);
102/// let weekdate = ISOWeekDate::new(2024, 10, Weekday::Saturday).unwrap();
103/// assert_eq!(d.iso_week_date(), weekdate);
104/// ```
105///
106/// # Example: overlapping leap and long years
107///
108/// A "long" ISO 8601 week year is a year with 53 weeks. That is, it is a year
109/// that includes a leap week. This example shows all years in the 20th
110/// century that are both Gregorian leap years and long years.
111///
112/// ```
113/// use jiff::civil::date;
114///
115/// let mut overlapping = vec![];
116/// for year in 1900..=1999 {
117/// let date = date(year, 1, 1);
118/// if date.in_leap_year() && date.iso_week_date().in_long_year() {
119/// overlapping.push(year);
120/// }
121/// }
122/// assert_eq!(overlapping, vec![
123/// 1904, 1908, 1920, 1932, 1936, 1948, 1960, 1964, 1976, 1988, 1992,
124/// ]);
125/// ```
126///
127/// # Example: printing all weeks in a year
128///
129/// The ISO 8601 week calendar can be useful when you want to categorize
130/// things into buckets of weeks where all weeks are exactly 7 days, _and_
131/// you don't care as much about the precise Gregorian year. Here's an example
132/// that prints all of the ISO 8601 weeks in one ISO 8601 week year:
133///
134/// ```
135/// use jiff::{civil::{ISOWeekDate, Weekday}, ToSpan};
136///
137/// let target_year = 2024;
138/// let iso_week_date = ISOWeekDate::new(target_year, 1, Weekday::Monday)?;
139/// // Create a series of dates via the Gregorian calendar. But since a
140/// // Gregorian week and an ISO 8601 week calendar week are both 7 days,
141/// // this works fine.
142/// let weeks = iso_week_date
143/// .date()
144/// .series(1.week())
145/// .map(|d| d.iso_week_date())
146/// .take_while(|wd| wd.year() == target_year);
147/// for start_of_week in weeks {
148/// let end_of_week = start_of_week.last_of_week()?;
149/// println!(
150/// "ISO week {}: {} - {}",
151/// start_of_week.week(),
152/// start_of_week.date(),
153/// end_of_week.date()
154/// );
155/// }
156/// # Ok::<(), Box<dyn std::error::Error>>(())
157/// ```
158#[derive(Clone, Copy, Hash)]
159pub struct ISOWeekDate {
160 year: ISOYear,
161 week: ISOWeek,
162 weekday: Weekday,
163}
164
165impl ISOWeekDate {
166 /// The maximum representable ISO week date.
167 ///
168 /// The maximum corresponds to the ISO week date of the maximum [`Date`]
169 /// value. That is, `-9999-01-01`.
170 pub const MIN: ISOWeekDate = ISOWeekDate {
171 year: ISOYear::new_unchecked(-9999),
172 week: ISOWeek::new_unchecked(1),
173 weekday: Weekday::Monday,
174 };
175
176 /// The minimum representable ISO week date.
177 ///
178 /// The minimum corresponds to the ISO week date of the minimum [`Date`]
179 /// value. That is, `9999-12-31`.
180 pub const MAX: ISOWeekDate = ISOWeekDate {
181 year: ISOYear::new_unchecked(9999),
182 week: ISOWeek::new_unchecked(52),
183 weekday: Weekday::Friday,
184 };
185
186 /// The first day of the zeroth year.
187 ///
188 /// This is guaranteed to be equivalent to `ISOWeekDate::default()`. Note
189 /// that this is not equivalent to `Date::default()`.
190 ///
191 /// # Example
192 ///
193 /// ```
194 /// use jiff::civil::{ISOWeekDate, date};
195 ///
196 /// assert_eq!(ISOWeekDate::ZERO, ISOWeekDate::default());
197 /// // The first day of the 0th year in the ISO week calendar is actually
198 /// // the third day of the 0th year in the proleptic Gregorian calendar!
199 /// assert_eq!(ISOWeekDate::default().date(), date(0, 1, 3));
200 /// ```
201 pub const ZERO: ISOWeekDate = ISOWeekDate {
202 year: ISOYear::new_unchecked(0),
203 week: ISOWeek::new_unchecked(1),
204 weekday: Weekday::Monday,
205 };
206
207 /// Create a new ISO week date from it constituent parts.
208 ///
209 /// If the given values are out of range (based on what is representable
210 /// as a [`Date`]), then this returns an error. This will also return an
211 /// error if a leap week is given (week number `53`) for a year that does
212 /// not contain a leap week.
213 ///
214 /// # Example
215 ///
216 /// This example shows some the boundary conditions involving minimum
217 /// and maximum dates:
218 ///
219 /// ```
220 /// use jiff::civil::{ISOWeekDate, Weekday, date};
221 ///
222 /// // The year 1949 does not contain a leap week.
223 /// assert!(ISOWeekDate::new(1949, 53, Weekday::Monday).is_err());
224 ///
225 /// // Examples of dates at or exceeding the maximum.
226 /// let max = ISOWeekDate::new(9999, 52, Weekday::Friday).unwrap();
227 /// assert_eq!(max, ISOWeekDate::MAX);
228 /// assert_eq!(max.date(), date(9999, 12, 31));
229 /// assert!(ISOWeekDate::new(9999, 52, Weekday::Saturday).is_err());
230 /// assert!(ISOWeekDate::new(9999, 53, Weekday::Monday).is_err());
231 ///
232 /// // Examples of dates at or exceeding the minimum.
233 /// let min = ISOWeekDate::new(-9999, 1, Weekday::Monday).unwrap();
234 /// assert_eq!(min, ISOWeekDate::MIN);
235 /// assert_eq!(min.date(), date(-9999, 1, 1));
236 /// assert!(ISOWeekDate::new(-10000, 52, Weekday::Sunday).is_err());
237 /// ```
238 #[inline]
239 pub fn new(
240 year: i16,
241 week: i8,
242 weekday: Weekday,
243 ) -> Result<ISOWeekDate, Error> {
244 let year = ISOYear::try_new("year", year)?;
245 let week = ISOWeek::try_new("week", week)?;
246 ISOWeekDate::new_ranged(year, week, weekday)
247 }
248
249 /// Converts a Gregorian date to an ISO week date.
250 ///
251 /// The minimum and maximum allowed values of an ISO week date are
252 /// set based on the minimum and maximum values of a `Date`. Therefore,
253 /// converting to and from `Date` values is non-lossy and infallible.
254 ///
255 /// This routine is equivalent to [`Date::iso_week_date`]. This routine
256 /// is also available via a `From<Date>` trait implementation for
257 /// `ISOWeekDate`.
258 ///
259 /// # Example
260 ///
261 /// ```
262 /// use jiff::civil::{ISOWeekDate, Weekday, date};
263 ///
264 /// let weekdate = ISOWeekDate::from_date(date(1948, 2, 10));
265 /// assert_eq!(
266 /// weekdate,
267 /// ISOWeekDate::new(1948, 7, Weekday::Tuesday).unwrap(),
268 /// );
269 /// ```
270 #[inline]
271 pub fn from_date(date: Date) -> ISOWeekDate {
272 date.iso_week_date()
273 }
274
275 // N.B. I tried defining a `ISOWeekDate::constant` for defining ISO week
276 // dates as constants, but it was too annoying to do. We could do it if
277 // there was a compelling reason for it though.
278
279 /// Returns the year component of this ISO 8601 week date.
280 ///
281 /// The value returned is guaranteed to be in the range `-9999..=9999`.
282 ///
283 /// # Example
284 ///
285 /// ```
286 /// use jiff::civil::date;
287 ///
288 /// let weekdate = date(2019, 12, 30).iso_week_date();
289 /// assert_eq!(weekdate.year(), 2020);
290 /// ```
291 #[inline]
292 pub fn year(self) -> i16 {
293 self.year_ranged().get()
294 }
295
296 /// Returns the week component of this ISO 8601 week date.
297 ///
298 /// The value returned is guaranteed to be in the range `1..=53`. A
299 /// value of `53` can only occur for "long" years. That is, years
300 /// with a leap week. This occurs precisely in cases for which
301 /// [`ISOWeekDate::in_long_year`] returns `true`.
302 ///
303 /// # Example
304 ///
305 /// ```
306 /// use jiff::civil::date;
307 ///
308 /// let weekdate = date(2019, 12, 30).iso_week_date();
309 /// assert_eq!(weekdate.year(), 2020);
310 /// assert_eq!(weekdate.week(), 1);
311 ///
312 /// let weekdate = date(1948, 12, 31).iso_week_date();
313 /// assert_eq!(weekdate.year(), 1948);
314 /// assert_eq!(weekdate.week(), 53);
315 /// ```
316 #[inline]
317 pub fn week(self) -> i8 {
318 self.week_ranged().get()
319 }
320
321 /// Returns the day component of this ISO 8601 week date.
322 ///
323 /// One can use methods on `Weekday` such as
324 /// [`Weekday::to_monday_one_offset`]
325 /// and
326 /// [`Weekday::to_sunday_zero_offset`]
327 /// to convert the weekday to a number.
328 ///
329 /// # Example
330 ///
331 /// ```
332 /// use jiff::civil::{date, Weekday};
333 ///
334 /// let weekdate = date(1948, 12, 31).iso_week_date();
335 /// assert_eq!(weekdate.year(), 1948);
336 /// assert_eq!(weekdate.week(), 53);
337 /// assert_eq!(weekdate.weekday(), Weekday::Friday);
338 /// assert_eq!(weekdate.weekday().to_monday_zero_offset(), 4);
339 /// assert_eq!(weekdate.weekday().to_monday_one_offset(), 5);
340 /// assert_eq!(weekdate.weekday().to_sunday_zero_offset(), 5);
341 /// assert_eq!(weekdate.weekday().to_sunday_one_offset(), 6);
342 /// ```
343 #[inline]
344 pub fn weekday(self) -> Weekday {
345 self.weekday
346 }
347
348 /// Returns the ISO 8601 week date corresponding to the first day in the
349 /// week of this week date. The date returned is guaranteed to have a
350 /// weekday of [`Weekday::Monday`].
351 ///
352 /// # Errors
353 ///
354 /// Since `-9999-01-01` falls on a Monday, it follows that the minimum
355 /// support Gregorian date is exactly equivalent to the minimum supported
356 /// ISO 8601 week date. This means that this routine can never actually
357 /// fail, but only insomuch as the minimums line up. For that reason, and
358 /// for consistency with [`ISOWeekDate::last_of_week`], the API is
359 /// fallible.
360 ///
361 /// # Example
362 ///
363 /// ```
364 /// use jiff::civil::{ISOWeekDate, Weekday, date};
365 ///
366 /// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
367 /// assert_eq!(wd.date(), date(2025, 1, 29));
368 /// assert_eq!(
369 /// wd.first_of_week()?,
370 /// ISOWeekDate::new(2025, 5, Weekday::Monday).unwrap(),
371 /// );
372 ///
373 /// // Works even for the minimum date.
374 /// assert_eq!(
375 /// ISOWeekDate::MIN.first_of_week()?,
376 /// ISOWeekDate::new(-9999, 1, Weekday::Monday).unwrap(),
377 /// );
378 ///
379 /// # Ok::<(), Box<dyn std::error::Error>>(())
380 /// ```
381 #[inline]
382 pub fn first_of_week(self) -> Result<ISOWeekDate, Error> {
383 // I believe this can never return an error because `Monday` is in
384 // bounds for all possible year-and-week combinations. This is *only*
385 // because -9999-01-01 corresponds to -9999-W01-Monday. Which is kinda
386 // lucky. And I guess if we ever change the ranges, this could become
387 // fallible.
388 ISOWeekDate::new_ranged(
389 self.year_ranged(),
390 self.week_ranged(),
391 Weekday::Monday,
392 )
393 }
394
395 /// Returns the ISO 8601 week date corresponding to the last day in the
396 /// week of this week date. The date returned is guaranteed to have a
397 /// weekday of [`Weekday::Sunday`].
398 ///
399 /// # Errors
400 ///
401 /// This can return an error if the last day of the week exceeds Jiff's
402 /// maximum Gregorian date of `9999-12-31`. It turns out this can happen
403 /// since `9999-12-31` falls on a Friday.
404 ///
405 /// # Example
406 ///
407 /// ```
408 /// use jiff::civil::{ISOWeekDate, Weekday, date};
409 ///
410 /// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
411 /// assert_eq!(wd.date(), date(2025, 1, 29));
412 /// assert_eq!(
413 /// wd.last_of_week()?,
414 /// ISOWeekDate::new(2025, 5, Weekday::Sunday).unwrap(),
415 /// );
416 ///
417 /// // Unlike `first_of_week`, this routine can actually fail on real
418 /// // values, although, only when close to the maximum supported date.
419 /// assert_eq!(
420 /// ISOWeekDate::MAX.last_of_week().unwrap_err().to_string(),
421 /// "parameter 'weekday' with value 7 is not \
422 /// in the required range of 1..=5",
423 /// );
424 ///
425 /// # Ok::<(), Box<dyn std::error::Error>>(())
426 /// ```
427 #[inline]
428 pub fn last_of_week(self) -> Result<ISOWeekDate, Error> {
429 // This can return an error when in the last week of the maximum year
430 // supported by Jiff. That's because the Saturday and Sunday of that
431 // week are actually in Gregorian year 10,000.
432 ISOWeekDate::new_ranged(
433 self.year_ranged(),
434 self.week_ranged(),
435 Weekday::Sunday,
436 )
437 }
438
439 /// Returns the ISO 8601 week date corresponding to the first day in the
440 /// year of this week date. The date returned is guaranteed to have a
441 /// weekday of [`Weekday::Monday`].
442 ///
443 /// # Errors
444 ///
445 /// Since `-9999-01-01` falls on a Monday, it follows that the minimum
446 /// support Gregorian date is exactly equivalent to the minimum supported
447 /// ISO 8601 week date. This means that this routine can never actually
448 /// fail, but only insomuch as the minimums line up. For that reason, and
449 /// for consistency with [`ISOWeekDate::last_of_year`], the API is
450 /// fallible.
451 ///
452 /// # Example
453 ///
454 /// ```
455 /// use jiff::civil::{ISOWeekDate, Weekday, date};
456 ///
457 /// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
458 /// assert_eq!(wd.date(), date(2025, 1, 29));
459 /// assert_eq!(
460 /// wd.first_of_year()?,
461 /// ISOWeekDate::new(2025, 1, Weekday::Monday).unwrap(),
462 /// );
463 ///
464 /// // Works even for the minimum date.
465 /// assert_eq!(
466 /// ISOWeekDate::MIN.first_of_year()?,
467 /// ISOWeekDate::new(-9999, 1, Weekday::Monday).unwrap(),
468 /// );
469 ///
470 /// # Ok::<(), Box<dyn std::error::Error>>(())
471 /// ```
472 #[inline]
473 pub fn first_of_year(self) -> Result<ISOWeekDate, Error> {
474 // I believe this can never return an error because `Monday` is in
475 // bounds for all possible years. This is *only* because -9999-01-01
476 // corresponds to -9999-W01-Monday. Which is kinda lucky. And I guess
477 // if we ever change the ranges, this could become fallible.
478 ISOWeekDate::new_ranged(self.year_ranged(), C(1), Weekday::Monday)
479 }
480
481 /// Returns the ISO 8601 week date corresponding to the last day in the
482 /// year of this week date. The date returned is guaranteed to have a
483 /// weekday of [`Weekday::Sunday`].
484 ///
485 /// # Errors
486 ///
487 /// This can return an error if the last day of the year exceeds Jiff's
488 /// maximum Gregorian date of `9999-12-31`. It turns out this can happen
489 /// since `9999-12-31` falls on a Friday.
490 ///
491 /// # Example
492 ///
493 /// ```
494 /// use jiff::civil::{ISOWeekDate, Weekday, date};
495 ///
496 /// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
497 /// assert_eq!(wd.date(), date(2025, 1, 29));
498 /// assert_eq!(
499 /// wd.last_of_year()?,
500 /// ISOWeekDate::new(2025, 52, Weekday::Sunday).unwrap(),
501 /// );
502 ///
503 /// // Works correctly for "long" years.
504 /// let wd = ISOWeekDate::new(2026, 5, Weekday::Wednesday).unwrap();
505 /// assert_eq!(wd.date(), date(2026, 1, 28));
506 /// assert_eq!(
507 /// wd.last_of_year()?,
508 /// ISOWeekDate::new(2026, 53, Weekday::Sunday).unwrap(),
509 /// );
510 ///
511 /// // Unlike `first_of_year`, this routine can actually fail on real
512 /// // values, although, only when close to the maximum supported date.
513 /// assert_eq!(
514 /// ISOWeekDate::MAX.last_of_year().unwrap_err().to_string(),
515 /// "parameter 'weekday' with value 7 is not \
516 /// in the required range of 1..=5",
517 /// );
518 ///
519 /// # Ok::<(), Box<dyn std::error::Error>>(())
520 /// ```
521 #[inline]
522 pub fn last_of_year(self) -> Result<ISOWeekDate, Error> {
523 // This can return an error when in the maximum year supported by
524 // Jiff. That's because the last Saturday and Sunday of that year are
525 // actually in Gregorian year 10,000.
526 let week = if self.in_long_year() {
527 ISOWeek::V::<53, 52, 53>()
528 } else {
529 ISOWeek::V::<52, 52, 53>()
530 };
531 ISOWeekDate::new_ranged(self.year_ranged(), week, Weekday::Sunday)
532 }
533
534 /// Returns the total number of days in the year of this ISO 8601 week
535 /// date.
536 ///
537 /// It is guaranteed that the value returned is either 364 or 371. The
538 /// latter case occurs precisely when [`ISOWeekDate::in_long_year`]
539 /// returns `true`.
540 ///
541 /// # Example
542 ///
543 /// ```
544 /// use jiff::civil::{ISOWeekDate, Weekday};
545 ///
546 /// let weekdate = ISOWeekDate::new(2025, 7, Weekday::Monday).unwrap();
547 /// assert_eq!(weekdate.days_in_year(), 364);
548 /// let weekdate = ISOWeekDate::new(2026, 7, Weekday::Monday).unwrap();
549 /// assert_eq!(weekdate.days_in_year(), 371);
550 /// ```
551 #[inline]
552 pub fn days_in_year(self) -> i16 {
553 if self.in_long_year() {
554 371
555 } else {
556 364
557 }
558 }
559
560 /// Returns the total number of weeks in the year of this ISO 8601 week
561 /// date.
562 ///
563 /// It is guaranteed that the value returned is either 52 or 53. The
564 /// latter case occurs precisely when [`ISOWeekDate::in_long_year`]
565 /// returns `true`.
566 ///
567 /// # Example
568 ///
569 /// ```
570 /// use jiff::civil::{ISOWeekDate, Weekday};
571 ///
572 /// let weekdate = ISOWeekDate::new(2025, 7, Weekday::Monday).unwrap();
573 /// assert_eq!(weekdate.weeks_in_year(), 52);
574 /// let weekdate = ISOWeekDate::new(2026, 7, Weekday::Monday).unwrap();
575 /// assert_eq!(weekdate.weeks_in_year(), 53);
576 /// ```
577 #[inline]
578 pub fn weeks_in_year(self) -> i8 {
579 if self.in_long_year() {
580 53
581 } else {
582 52
583 }
584 }
585
586 /// Returns true if and only if the year of this week date is a "long"
587 /// year.
588 ///
589 /// A long year is one that contains precisely 53 weeks. All other years
590 /// contain precisely 52 weeks.
591 ///
592 /// # Example
593 ///
594 /// ```
595 /// use jiff::civil::{ISOWeekDate, Weekday};
596 ///
597 /// let weekdate = ISOWeekDate::new(1948, 7, Weekday::Monday).unwrap();
598 /// assert!(weekdate.in_long_year());
599 /// let weekdate = ISOWeekDate::new(1949, 7, Weekday::Monday).unwrap();
600 /// assert!(!weekdate.in_long_year());
601 /// ```
602 #[inline]
603 pub fn in_long_year(self) -> bool {
604 is_long_year(self.year_ranged())
605 }
606
607 /// Returns the ISO 8601 date immediately following this one.
608 ///
609 /// # Errors
610 ///
611 /// This returns an error when this date is the maximum value.
612 ///
613 /// # Example
614 ///
615 /// ```
616 /// use jiff::civil::{ISOWeekDate, Weekday};
617 ///
618 /// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
619 /// assert_eq!(
620 /// wd.tomorrow()?,
621 /// ISOWeekDate::new(2025, 5, Weekday::Thursday).unwrap(),
622 /// );
623 ///
624 /// // The max doesn't have a tomorrow.
625 /// assert!(ISOWeekDate::MAX.tomorrow().is_err());
626 ///
627 /// # Ok::<(), Box<dyn std::error::Error>>(())
628 /// ```
629 #[inline]
630 pub fn tomorrow(self) -> Result<ISOWeekDate, Error> {
631 // I suppose we could probably implement this in a more efficient
632 // manner but avoiding the roundtrip through Gregorian dates.
633 self.date().tomorrow().map(|d| d.iso_week_date())
634 }
635
636 /// Returns the ISO 8601 week date immediately preceding this one.
637 ///
638 /// # Errors
639 ///
640 /// This returns an error when this date is the minimum value.
641 ///
642 /// # Example
643 ///
644 /// ```
645 /// use jiff::civil::{ISOWeekDate, Weekday};
646 ///
647 /// let wd = ISOWeekDate::new(2025, 5, Weekday::Wednesday).unwrap();
648 /// assert_eq!(
649 /// wd.yesterday()?,
650 /// ISOWeekDate::new(2025, 5, Weekday::Tuesday).unwrap(),
651 /// );
652 ///
653 /// // The min doesn't have a yesterday.
654 /// assert!(ISOWeekDate::MIN.yesterday().is_err());
655 ///
656 /// # Ok::<(), Box<dyn std::error::Error>>(())
657 /// ```
658 #[inline]
659 pub fn yesterday(self) -> Result<ISOWeekDate, Error> {
660 // I suppose we could probably implement this in a more efficient
661 // manner but avoiding the roundtrip through Gregorian dates.
662 self.date().yesterday().map(|d| d.iso_week_date())
663 }
664
665 /// Converts this ISO week date to a Gregorian [`Date`].
666 ///
667 /// The minimum and maximum allowed values of an ISO week date are
668 /// set based on the minimum and maximum values of a `Date`. Therefore,
669 /// converting to and from `Date` values is non-lossy and infallible.
670 ///
671 /// This routine is equivalent to [`Date::from_iso_week_date`].
672 ///
673 /// # Example
674 ///
675 /// ```
676 /// use jiff::civil::{ISOWeekDate, Weekday, date};
677 ///
678 /// let weekdate = ISOWeekDate::new(1948, 7, Weekday::Tuesday).unwrap();
679 /// assert_eq!(weekdate.date(), date(1948, 2, 10));
680 /// ```
681 #[inline]
682 pub fn date(self) -> Date {
683 Date::from_iso_week_date(self)
684 }
685}
686
687impl ISOWeekDate {
688 /// Creates a new ISO week date from ranged values.
689 ///
690 /// While the ranged values given eliminate some error cases, not all
691 /// combinations of year/week/weekday values are valid ISO week dates
692 /// supported by this crate. For example, a week of `53` for short years,
693 /// or more niche, a week date that would be bigger than what is supported
694 /// by our `Date` type.
695 #[inline]
696 pub(crate) fn new_ranged(
697 year: impl RInto<ISOYear>,
698 week: impl RInto<ISOWeek>,
699 weekday: Weekday,
700 ) -> Result<ISOWeekDate, Error> {
701 let year = year.rinto();
702 let week = week.rinto();
703 // All combinations of years, weeks and weekdays allowed by our
704 // range types are valid ISO week dates with one exception: a week
705 // number of 53 is only valid for "long" years. Or years with an ISO
706 // leap week. It turns out this only happens when the last day of the
707 // year is a Thursday.
708 //
709 // Note that if the ranges in this crate are changed, this could be
710 // a little trickier if the range of ISOYear is different from Year.
711 debug_assert_eq!(t::Year::MIN, ISOYear::MIN);
712 debug_assert_eq!(t::Year::MAX, ISOYear::MAX);
713 if week == C(53) && !is_long_year(year) {
714 return Err(Error::from(E::InvalidISOWeekNumber));
715 }
716 // And also, the maximum Date constrains what we can utter with
717 // ISOWeekDate so that we can preserve infallible conversions between
718 // them. So since 9999-12-31 maps to 9999 W52 Friday, it follows that
719 // Saturday and Sunday are not allowed. So reject them.
720 //
721 // We don't need to worry about the minimum because the minimum date
722 // (-9999-01-01) corresponds also to the minimum possible combination
723 // of an ISO week date's fields: -9999 W01 Monday. Nice.
724 if year == ISOYear::MAX_SELF
725 && week == C(52)
726 && weekday.to_monday_zero_offset()
727 > Weekday::Friday.to_monday_zero_offset()
728 {
729 return Err(Error::range(
730 "weekday",
731 weekday.to_monday_one_offset(),
732 Weekday::Monday.to_monday_one_offset(),
733 Weekday::Friday.to_monday_one_offset(),
734 ));
735 }
736 Ok(ISOWeekDate { year, week, weekday })
737 }
738
739 /// Like `ISOWeekDate::new_ranged`, but constrains out-of-bounds values
740 /// to their closest valid equivalent.
741 ///
742 /// For example, given 9999 W52 Saturday, this will return 9999 W52 Friday.
743 #[cfg(test)]
744 #[inline]
745 pub(crate) fn new_ranged_constrain(
746 year: impl RInto<ISOYear>,
747 week: impl RInto<ISOWeek>,
748 mut weekday: Weekday,
749 ) -> ISOWeekDate {
750 let year = year.rinto();
751 let mut week = week.rinto();
752 debug_assert_eq!(t::Year::MIN, ISOYear::MIN);
753 debug_assert_eq!(t::Year::MAX, ISOYear::MAX);
754 if week == C(53) && !is_long_year(year) {
755 week = ISOWeek::new(52).unwrap();
756 }
757 if year == ISOYear::MAX_SELF
758 && week == C(52)
759 && weekday.to_monday_zero_offset()
760 > Weekday::Friday.to_monday_zero_offset()
761 {
762 weekday = Weekday::Friday;
763 }
764 ISOWeekDate { year, week, weekday }
765 }
766
767 #[inline]
768 pub(crate) fn year_ranged(self) -> ISOYear {
769 self.year
770 }
771
772 #[inline]
773 pub(crate) fn week_ranged(self) -> ISOWeek {
774 self.week
775 }
776}
777
778impl Default for ISOWeekDate {
779 fn default() -> ISOWeekDate {
780 ISOWeekDate::ZERO
781 }
782}
783
784impl core::fmt::Debug for ISOWeekDate {
785 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
786 f.debug_struct("ISOWeekDate")
787 .field("year", &self.year_ranged().debug())
788 .field("week", &self.week_ranged().debug())
789 .field("weekday", &self.weekday)
790 .finish()
791 }
792}
793
794impl core::fmt::Display for ISOWeekDate {
795 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
796 use crate::fmt::StdFmtWrite;
797
798 DEFAULT_DATETIME_PRINTER
799 .print_iso_week_date(self, StdFmtWrite(f))
800 .map_err(|_| core::fmt::Error)
801 }
802}
803
804impl core::str::FromStr for ISOWeekDate {
805 type Err = Error;
806
807 fn from_str(string: &str) -> Result<ISOWeekDate, Error> {
808 DEFAULT_DATETIME_PARSER.parse_iso_week_date(string)
809 }
810}
811
812impl Eq for ISOWeekDate {}
813
814impl PartialEq for ISOWeekDate {
815 #[inline]
816 fn eq(&self, other: &ISOWeekDate) -> bool {
817 // We roll our own so that we can call 'get' on our ranged integers
818 // in order to provoke panics for bugs in dealing with boundary
819 // conditions.
820 self.weekday == other.weekday
821 && self.week.get() == other.week.get()
822 && self.year.get() == other.year.get()
823 }
824}
825
826impl Ord for ISOWeekDate {
827 #[inline]
828 fn cmp(&self, other: &ISOWeekDate) -> core::cmp::Ordering {
829 (self.year.get(), self.week.get(), self.weekday.to_monday_one_offset())
830 .cmp(&(
831 other.year.get(),
832 other.week.get(),
833 other.weekday.to_monday_one_offset(),
834 ))
835 }
836}
837
838impl PartialOrd for ISOWeekDate {
839 #[inline]
840 fn partial_cmp(&self, other: &ISOWeekDate) -> Option<core::cmp::Ordering> {
841 Some(self.cmp(other))
842 }
843}
844
845impl From<Date> for ISOWeekDate {
846 #[inline]
847 fn from(date: Date) -> ISOWeekDate {
848 ISOWeekDate::from_date(date)
849 }
850}
851
852impl From<DateTime> for ISOWeekDate {
853 #[inline]
854 fn from(dt: DateTime) -> ISOWeekDate {
855 ISOWeekDate::from(dt.date())
856 }
857}
858
859impl From<Zoned> for ISOWeekDate {
860 #[inline]
861 fn from(zdt: Zoned) -> ISOWeekDate {
862 ISOWeekDate::from(zdt.date())
863 }
864}
865
866impl<'a> From<&'a Zoned> for ISOWeekDate {
867 #[inline]
868 fn from(zdt: &'a Zoned) -> ISOWeekDate {
869 ISOWeekDate::from(zdt.date())
870 }
871}
872
873#[cfg(feature = "serde")]
874impl serde_core::Serialize for ISOWeekDate {
875 #[inline]
876 fn serialize<S: serde_core::Serializer>(
877 &self,
878 serializer: S,
879 ) -> Result<S::Ok, S::Error> {
880 serializer.collect_str(self)
881 }
882}
883
884#[cfg(feature = "serde")]
885impl<'de> serde_core::Deserialize<'de> for ISOWeekDate {
886 #[inline]
887 fn deserialize<D: serde_core::Deserializer<'de>>(
888 deserializer: D,
889 ) -> Result<ISOWeekDate, D::Error> {
890 use serde_core::de;
891
892 struct ISOWeekDateVisitor;
893
894 impl<'de> de::Visitor<'de> for ISOWeekDateVisitor {
895 type Value = ISOWeekDate;
896
897 fn expecting(
898 &self,
899 f: &mut core::fmt::Formatter,
900 ) -> core::fmt::Result {
901 f.write_str("an ISO 8601 week date string")
902 }
903
904 #[inline]
905 fn visit_bytes<E: de::Error>(
906 self,
907 value: &[u8],
908 ) -> Result<ISOWeekDate, E> {
909 DEFAULT_DATETIME_PARSER
910 .parse_iso_week_date(value)
911 .map_err(de::Error::custom)
912 }
913
914 #[inline]
915 fn visit_str<E: de::Error>(
916 self,
917 value: &str,
918 ) -> Result<ISOWeekDate, E> {
919 self.visit_bytes(value.as_bytes())
920 }
921 }
922
923 deserializer.deserialize_str(ISOWeekDateVisitor)
924 }
925}
926
927#[cfg(test)]
928impl quickcheck::Arbitrary for ISOWeekDate {
929 fn arbitrary(g: &mut quickcheck::Gen) -> ISOWeekDate {
930 let year = ISOYear::arbitrary(g);
931 let week = ISOWeek::arbitrary(g);
932 let weekday = Weekday::arbitrary(g);
933 ISOWeekDate::new_ranged_constrain(year, week, weekday)
934 }
935
936 fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = ISOWeekDate>> {
937 alloc::boxed::Box::new(
938 (self.year_ranged(), self.week_ranged(), self.weekday())
939 .shrink()
940 .map(|(year, week, weekday)| {
941 ISOWeekDate::new_ranged_constrain(year, week, weekday)
942 }),
943 )
944 }
945}
946
947/// Returns true if the given ISO year is a "long" year or not.
948///
949/// A "long" year is a year with 53 weeks. Otherwise, it's a "short" year
950/// with 52 weeks.
951fn is_long_year(year: ISOYear) -> bool {
952 // Inspired by: https://en.wikipedia.org/wiki/ISO_week_date#Weeks_per_year
953 let last = Date::new_ranged(year.rinto(), C(12).rinto(), C(31).rinto())
954 .expect("last day of year is always valid");
955 let weekday = last.weekday();
956 weekday == Weekday::Thursday
957 || (last.in_leap_year() && weekday == Weekday::Friday)
958}
959
960#[cfg(not(miri))]
961#[cfg(test)]
962mod tests {
963 use super::*;
964
965 quickcheck::quickcheck! {
966 fn prop_all_long_years_have_53rd_week(year: ISOYear) -> bool {
967 !is_long_year(year)
968 || ISOWeekDate::new(year.get(), 53, Weekday::Sunday).is_ok()
969 }
970
971 fn prop_prev_day_is_less(wd: ISOWeekDate) -> quickcheck::TestResult {
972 use crate::ToSpan;
973
974 if wd == ISOWeekDate::MIN {
975 return quickcheck::TestResult::discard();
976 }
977 let prev_date = wd.date().checked_add(-1.days()).unwrap();
978 quickcheck::TestResult::from_bool(prev_date.iso_week_date() < wd)
979 }
980
981 fn prop_next_day_is_greater(wd: ISOWeekDate) -> quickcheck::TestResult {
982 use crate::ToSpan;
983
984 if wd == ISOWeekDate::MAX {
985 return quickcheck::TestResult::discard();
986 }
987 let next_date = wd.date().checked_add(1.days()).unwrap();
988 quickcheck::TestResult::from_bool(wd < next_date.iso_week_date())
989 }
990 }
991}