1use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
19use crate::error::DateError;
20use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError};
21use calendrical_calculations::helpers::I32CastError;
22use calendrical_calculations::rata_die::RataDie;
23use tinystr::tinystr;
24
25#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
38#[allow(clippy::exhaustive_structs)] pub struct Iso;
40
41#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
42pub struct IsoDateInner(pub(crate) ArithmeticDate<Iso>);
44
45impl CalendarArithmetic for Iso {
46 type YearInfo = i32;
47
48 fn days_in_provided_month(year: i32, month: u8) -> u8 {
49 match month {
50 4 | 6 | 9 | 11 => 30,
51 2 if Self::provided_year_is_leap(year) => 29,
52 2 => 28,
53 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
54 _ => 0,
55 }
56 }
57
58 fn months_in_provided_year(_: i32) -> u8 {
59 12
60 }
61
62 fn provided_year_is_leap(year: i32) -> bool {
63 calendrical_calculations::iso::is_leap_year(year)
64 }
65
66 fn last_month_day_in_provided_year(_year: i32) -> (u8, u8) {
67 (12, 31)
68 }
69
70 fn days_in_provided_year(year: i32) -> u16 {
71 if Self::provided_year_is_leap(year) {
72 366
73 } else {
74 365
75 }
76 }
77}
78
79impl crate::cal::scaffold::UnstableSealed for Iso {}
80impl Calendar for Iso {
81 type DateInner = IsoDateInner;
82 type Year = types::EraYear;
83 fn from_codes(
85 &self,
86 era: Option<&str>,
87 year: i32,
88 month_code: types::MonthCode,
89 day: u8,
90 ) -> Result<Self::DateInner, DateError> {
91 let year = match era {
92 Some("default") | None => year,
93 Some(_) => return Err(DateError::UnknownEra),
94 };
95
96 ArithmeticDate::new_from_codes(self, year, month_code, day).map(IsoDateInner)
97 }
98
99 fn from_rata_die(&self, date: RataDie) -> IsoDateInner {
100 IsoDateInner(match calendrical_calculations::iso::iso_from_fixed(date) {
101 Err(I32CastError::BelowMin) => ArithmeticDate::min_date(),
102 Err(I32CastError::AboveMax) => ArithmeticDate::max_date(),
103 Ok((year, month, day)) => ArithmeticDate::new_unchecked(year, month, day),
104 })
105 }
106
107 fn to_rata_die(&self, date: &IsoDateInner) -> RataDie {
108 calendrical_calculations::iso::fixed_from_iso(date.0.year, date.0.month, date.0.day)
109 }
110
111 fn from_iso(&self, iso: IsoDateInner) -> IsoDateInner {
112 iso
113 }
114
115 fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
116 *date
117 }
118
119 fn months_in_year(&self, date: &Self::DateInner) -> u8 {
120 date.0.months_in_year()
121 }
122
123 fn days_in_year(&self, date: &Self::DateInner) -> u16 {
124 date.0.days_in_year()
125 }
126
127 fn days_in_month(&self, date: &Self::DateInner) -> u8 {
128 date.0.days_in_month()
129 }
130
131 fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
132 date.0.offset_date(offset, &());
133 }
134
135 #[allow(clippy::field_reassign_with_default)]
136 fn until(
137 &self,
138 date1: &Self::DateInner,
139 date2: &Self::DateInner,
140 _calendar2: &Self,
141 _largest_unit: DateDurationUnit,
142 _smallest_unit: DateDurationUnit,
143 ) -> DateDuration<Self> {
144 date1.0.until(date2.0, _largest_unit, _smallest_unit)
145 }
146
147 fn year_info(&self, date: &Self::DateInner) -> Self::Year {
148 types::EraYear {
149 era_index: Some(0),
150 era: tinystr!(16, "default"),
151 year: self.extended_year(date),
152 ambiguity: types::YearAmbiguity::Unambiguous,
153 }
154 }
155
156 fn extended_year(&self, date: &Self::DateInner) -> i32 {
157 date.0.extended_year()
158 }
159
160 fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
161 Self::provided_year_is_leap(date.0.year)
162 }
163
164 fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
166 date.0.month()
167 }
168
169 fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
171 date.0.day_of_month()
172 }
173
174 fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
175 date.0.day_of_year()
176 }
177
178 fn debug_name(&self) -> &'static str {
179 "ISO"
180 }
181
182 fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
183 None
184 }
185}
186
187impl Date<Iso> {
188 pub fn try_new_iso(year: i32, month: u8, day: u8) -> Result<Date<Iso>, RangeError> {
201 ArithmeticDate::new_from_ordinals(year, month, day)
202 .map(IsoDateInner)
203 .map(|inner| Date::from_raw(inner, Iso))
204 }
205}
206
207impl Iso {
208 pub fn new() -> Self {
210 Self
211 }
212
213 pub(crate) fn iso_from_year_day(year: i32, year_day: u16) -> IsoDateInner {
214 let mut month = 1;
215 let mut day = year_day as i32;
216 while month <= 12 {
217 let month_days = Self::days_in_provided_month(year, month) as i32;
218 if day <= month_days {
219 break;
220 } else {
221 debug_assert!(month < 12); day -= month_days;
223 month += 1;
224 }
225 }
226 let day = day as u8; IsoDateInner(ArithmeticDate::new_unchecked(year, month, day))
230 }
231
232 pub(crate) fn day_of_year(date: IsoDateInner) -> u16 {
233 let month_offset = [0, 1, -1, 0, 0, 1, 1, 2, 3, 3, 4, 4];
236 #[allow(clippy::indexing_slicing)] let mut offset = month_offset[date.0.month as usize - 1];
238 if Self::provided_year_is_leap(date.0.year) && date.0.month > 2 {
239 offset += 1;
241 }
242 let prev_month_days = (30 * (date.0.month as i32 - 1) + offset) as u16;
243
244 prev_month_days + date.0.day as u16
245 }
246}
247
248#[cfg(test)]
249mod test {
250 use super::*;
251 use crate::types::Weekday;
252
253 #[test]
254 fn iso_overflow() {
255 #[derive(Debug)]
256 struct TestCase {
257 year: i32,
258 month: u8,
259 day: u8,
260 rd: RataDie,
261 saturating: bool,
262 }
263 let max_year = Iso.from_rata_die(RataDie::new(i32::MAX as i64)).0.year;
265
266 let min_year = -5879610;
269
270 let cases = [
271 TestCase {
272 year: min_year,
274 month: 6,
275 day: 22,
276 rd: RataDie::new(i32::MIN as i64),
277 saturating: false,
278 },
279 TestCase {
280 year: min_year,
281 month: 6,
282 day: 23,
283 rd: RataDie::new(i32::MIN as i64 + 1),
284 saturating: false,
285 },
286 TestCase {
287 year: min_year,
288 month: 6,
289 day: 21,
290 rd: RataDie::new(i32::MIN as i64 - 1),
291 saturating: false,
292 },
293 TestCase {
294 year: min_year,
295 month: 12,
296 day: 31,
297 rd: RataDie::new(-2147483456),
298 saturating: false,
299 },
300 TestCase {
301 year: min_year + 1,
302 month: 1,
303 day: 1,
304 rd: RataDie::new(-2147483455),
305 saturating: false,
306 },
307 TestCase {
308 year: max_year,
309 month: 6,
310 day: 11,
311 rd: RataDie::new(i32::MAX as i64 - 30),
312 saturating: false,
313 },
314 TestCase {
315 year: max_year,
316 month: 7,
317 day: 9,
318 rd: RataDie::new(i32::MAX as i64 - 2),
319 saturating: false,
320 },
321 TestCase {
322 year: max_year,
323 month: 7,
324 day: 10,
325 rd: RataDie::new(i32::MAX as i64 - 1),
326 saturating: false,
327 },
328 TestCase {
329 year: max_year,
331 month: 7,
332 day: 11,
333 rd: RataDie::new(i32::MAX as i64),
334 saturating: false,
335 },
336 TestCase {
337 year: max_year,
338 month: 7,
339 day: 12,
340 rd: RataDie::new(i32::MAX as i64 + 1),
341 saturating: false,
342 },
343 TestCase {
344 year: i32::MIN,
345 month: 1,
346 day: 2,
347 rd: RataDie::new(-784352296669),
348 saturating: false,
349 },
350 TestCase {
351 year: i32::MIN,
352 month: 1,
353 day: 1,
354 rd: RataDie::new(-784352296670),
355 saturating: false,
356 },
357 TestCase {
358 year: i32::MIN,
359 month: 1,
360 day: 1,
361 rd: RataDie::new(-784352296671),
362 saturating: true,
363 },
364 TestCase {
365 year: i32::MAX,
366 month: 12,
367 day: 30,
368 rd: RataDie::new(784352295938),
369 saturating: false,
370 },
371 TestCase {
372 year: i32::MAX,
373 month: 12,
374 day: 31,
375 rd: RataDie::new(784352295939),
376 saturating: false,
377 },
378 TestCase {
379 year: i32::MAX,
380 month: 12,
381 day: 31,
382 rd: RataDie::new(784352295940),
383 saturating: true,
384 },
385 ];
386
387 for case in cases {
388 let date = Date::try_new_iso(case.year, case.month, case.day).unwrap();
389 if !case.saturating {
390 assert_eq!(date.to_rata_die(), case.rd, "{case:?}");
391 }
392 assert_eq!(Date::from_rata_die(case.rd, Iso), date, "{case:?}");
393 }
394 }
395
396 #[test]
398 fn min_year() {
399 assert_eq!(
400 Date::from_rata_die(RataDie::big_negative(), Iso)
401 .year()
402 .era()
403 .unwrap()
404 .year,
405 i32::MIN
406 );
407 }
408
409 #[test]
410 fn test_day_of_week() {
411 assert_eq!(
413 Date::try_new_iso(2021, 6, 23).unwrap().day_of_week(),
414 Weekday::Wednesday,
415 );
416 assert_eq!(
418 Date::try_new_iso(1983, 2, 2).unwrap().day_of_week(),
419 Weekday::Wednesday,
420 );
421 assert_eq!(
423 Date::try_new_iso(2020, 1, 21).unwrap().day_of_week(),
424 Weekday::Tuesday,
425 );
426 }
427
428 #[test]
429 fn test_day_of_year() {
430 assert_eq!(Date::try_new_iso(2021, 6, 23).unwrap().day_of_year().0, 174,);
432 assert_eq!(Date::try_new_iso(2020, 6, 23).unwrap().day_of_year().0, 175,);
434 assert_eq!(Date::try_new_iso(1983, 2, 2).unwrap().day_of_year().0, 33,);
436 }
437
438 fn simple_subtract(a: &Date<Iso>, b: &Date<Iso>) -> DateDuration<Iso> {
439 let a = a.inner();
440 let b = b.inner();
441 DateDuration::new(
442 a.0.year - b.0.year,
443 a.0.month as i32 - b.0.month as i32,
444 0,
445 a.0.day as i32 - b.0.day as i32,
446 )
447 }
448
449 #[test]
450 fn test_offset() {
451 let today = Date::try_new_iso(2021, 6, 23).unwrap();
452 let today_plus_5000 = Date::try_new_iso(2035, 3, 2).unwrap();
453 let offset = today.added(DateDuration::new(0, 0, 0, 5000));
454 assert_eq!(offset, today_plus_5000);
455 let offset = today.added(simple_subtract(&today_plus_5000, &today));
456 assert_eq!(offset, today_plus_5000);
457
458 let today = Date::try_new_iso(2021, 6, 23).unwrap();
459 let today_minus_5000 = Date::try_new_iso(2007, 10, 15).unwrap();
460 let offset = today.added(DateDuration::new(0, 0, 0, -5000));
461 assert_eq!(offset, today_minus_5000);
462 let offset = today.added(simple_subtract(&today_minus_5000, &today));
463 assert_eq!(offset, today_minus_5000);
464 }
465
466 #[test]
467 fn test_offset_at_month_boundary() {
468 let today = Date::try_new_iso(2020, 2, 28).unwrap();
469 let today_plus_2 = Date::try_new_iso(2020, 3, 1).unwrap();
470 let offset = today.added(DateDuration::new(0, 0, 0, 2));
471 assert_eq!(offset, today_plus_2);
472
473 let today = Date::try_new_iso(2020, 2, 28).unwrap();
474 let today_plus_3 = Date::try_new_iso(2020, 3, 2).unwrap();
475 let offset = today.added(DateDuration::new(0, 0, 0, 3));
476 assert_eq!(offset, today_plus_3);
477
478 let today = Date::try_new_iso(2020, 2, 28).unwrap();
479 let today_plus_1 = Date::try_new_iso(2020, 2, 29).unwrap();
480 let offset = today.added(DateDuration::new(0, 0, 0, 1));
481 assert_eq!(offset, today_plus_1);
482
483 let today = Date::try_new_iso(2019, 2, 28).unwrap();
484 let today_plus_2 = Date::try_new_iso(2019, 3, 2).unwrap();
485 let offset = today.added(DateDuration::new(0, 0, 0, 2));
486 assert_eq!(offset, today_plus_2);
487
488 let today = Date::try_new_iso(2019, 2, 28).unwrap();
489 let today_plus_1 = Date::try_new_iso(2019, 3, 1).unwrap();
490 let offset = today.added(DateDuration::new(0, 0, 0, 1));
491 assert_eq!(offset, today_plus_1);
492
493 let today = Date::try_new_iso(2020, 3, 1).unwrap();
494 let today_minus_1 = Date::try_new_iso(2020, 2, 29).unwrap();
495 let offset = today.added(DateDuration::new(0, 0, 0, -1));
496 assert_eq!(offset, today_minus_1);
497 }
498
499 #[test]
500 fn test_offset_handles_negative_month_offset() {
501 let today = Date::try_new_iso(2020, 3, 1).unwrap();
502 let today_minus_2_months = Date::try_new_iso(2020, 1, 1).unwrap();
503 let offset = today.added(DateDuration::new(0, -2, 0, 0));
504 assert_eq!(offset, today_minus_2_months);
505
506 let today = Date::try_new_iso(2020, 3, 1).unwrap();
507 let today_minus_4_months = Date::try_new_iso(2019, 11, 1).unwrap();
508 let offset = today.added(DateDuration::new(0, -4, 0, 0));
509 assert_eq!(offset, today_minus_4_months);
510
511 let today = Date::try_new_iso(2020, 3, 1).unwrap();
512 let today_minus_24_months = Date::try_new_iso(2018, 3, 1).unwrap();
513 let offset = today.added(DateDuration::new(0, -24, 0, 0));
514 assert_eq!(offset, today_minus_24_months);
515
516 let today = Date::try_new_iso(2020, 3, 1).unwrap();
517 let today_minus_27_months = Date::try_new_iso(2017, 12, 1).unwrap();
518 let offset = today.added(DateDuration::new(0, -27, 0, 0));
519 assert_eq!(offset, today_minus_27_months);
520 }
521
522 #[test]
523 fn test_offset_handles_out_of_bound_month_offset() {
524 let today = Date::try_new_iso(2021, 1, 31).unwrap();
525 let today_plus_1_month = Date::try_new_iso(2021, 3, 3).unwrap();
527 let offset = today.added(DateDuration::new(0, 1, 0, 0));
528 assert_eq!(offset, today_plus_1_month);
529
530 let today = Date::try_new_iso(2021, 1, 31).unwrap();
531 let today_plus_1_month_1_day = Date::try_new_iso(2021, 3, 4).unwrap();
533 let offset = today.added(DateDuration::new(0, 1, 0, 1));
534 assert_eq!(offset, today_plus_1_month_1_day);
535 }
536
537 #[test]
538 fn test_iso_to_from_rd() {
539 fn check(rd: i64, year: i32, month: u8, day: u8) {
542 let rd = RataDie::new(rd);
543
544 assert_eq!(
545 Date::from_rata_die(rd, Iso),
546 Date::try_new_iso(year, month, day).unwrap(),
547 "RD: {rd:?}"
548 );
549 }
550 check(-1828, -5, 12, 30);
551 check(-1827, -5, 12, 31); check(-1826, -4, 1, 1);
553 check(-1462, -4, 12, 30);
554 check(-1461, -4, 12, 31);
555 check(-1460, -3, 1, 1);
556 check(-1459, -3, 1, 2);
557 check(-732, -2, 12, 30);
558 check(-731, -2, 12, 31);
559 check(-730, -1, 1, 1);
560 check(-367, -1, 12, 30);
561 check(-366, -1, 12, 31);
562 check(-365, 0, 1, 1); check(-364, 0, 1, 2);
564 check(-1, 0, 12, 30);
565 check(0, 0, 12, 31);
566 check(1, 1, 1, 1);
567 check(2, 1, 1, 2);
568 check(364, 1, 12, 30);
569 check(365, 1, 12, 31);
570 check(366, 2, 1, 1);
571 check(1459, 4, 12, 29);
572 check(1460, 4, 12, 30);
573 check(1461, 4, 12, 31); check(1462, 5, 1, 1);
575 }
576}