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