1use crate::error::CalendarError;
8use core::convert::TryFrom;
9use core::convert::TryInto;
10use core::fmt;
11use core::num::NonZeroU8;
12use core::str::FromStr;
13use tinystr::TinyAsciiStr;
14use tinystr::{TinyStr16, TinyStr4};
15use zerovec::maps::ZeroMapKV;
16use zerovec::ule::AsULE;
17
18#[derive(Copy, Clone, Debug, PartialEq)]
27#[allow(clippy::exhaustive_structs)] pub struct Era(pub TinyStr16);
29
30impl From<TinyStr16> for Era {
31 fn from(x: TinyStr16) -> Self {
32 Self(x)
33 }
34}
35
36impl FromStr for Era {
37 type Err = <TinyStr16 as FromStr>::Err;
38 fn from_str(s: &str) -> Result<Self, Self::Err> {
39 s.parse().map(Self)
40 }
41}
42
43#[derive(Copy, Clone, Debug, PartialEq)]
47#[non_exhaustive]
48pub struct FormattableYear {
49 pub era: Era,
55
56 pub number: i32,
58
59 pub cyclic: Option<NonZeroU8>,
65
66 pub related_iso: Option<i32>,
72}
73
74impl FormattableYear {
75 pub fn new(era: Era, number: i32, cyclic: Option<NonZeroU8>) -> Self {
80 Self {
81 era,
82 number,
83 cyclic,
84 related_iso: None,
85 }
86 }
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
100#[allow(clippy::exhaustive_structs)] #[cfg_attr(
102 feature = "datagen",
103 derive(serde::Serialize, databake::Bake),
104 databake(path = icu_calendar::types),
105)]
106#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
107pub struct MonthCode(pub TinyStr4);
108
109impl MonthCode {
110 pub fn get_normal_if_leap(self) -> Option<MonthCode> {
114 let bytes = self.0.all_bytes();
115 if bytes[3] == b'L' {
116 Some(MonthCode(TinyAsciiStr::from_bytes(&bytes[0..3]).ok()?))
117 } else {
118 None
119 }
120 }
121 pub fn parsed(self) -> Option<(u8, bool)> {
123 let bytes = self.0.all_bytes();
127 let is_leap = bytes[3] == b'L';
128 if bytes[0] != b'M' {
129 return None;
130 }
131 if bytes[1] == b'0' {
132 if bytes[2] >= b'1' && bytes[2] <= b'9' {
133 return Some((bytes[2] - b'0', is_leap));
134 }
135 } else if bytes[1] == b'1' && bytes[2] >= b'0' && bytes[2] <= b'3' {
136 return Some((10 + bytes[2] - b'0', is_leap));
137 }
138 None
139 }
140
141 #[cfg(test)] pub(crate) fn new_normal(number: u8) -> Option<Self> {
146 let tens = number / 10;
147 let ones = number % 10;
148 if tens > 9 {
149 return None;
150 }
151
152 let bytes = [b'M', b'0' + tens, b'0' + ones, 0];
153 Some(MonthCode(TinyAsciiStr::try_from_raw(bytes).ok()?))
154 }
155}
156
157#[test]
158fn test_get_normal_month_code_if_leap() {
159 let mc1 = MonthCode(tinystr::tinystr!(4, "M01L"));
160 let result1 = mc1.get_normal_if_leap();
161 assert_eq!(result1, Some(MonthCode(tinystr::tinystr!(4, "M01"))));
162
163 let mc2 = MonthCode(tinystr::tinystr!(4, "M11L"));
164 let result2 = mc2.get_normal_if_leap();
165 assert_eq!(result2, Some(MonthCode(tinystr::tinystr!(4, "M11"))));
166
167 let mc_invalid = MonthCode(tinystr::tinystr!(4, "M10"));
168 let result_invalid = mc_invalid.get_normal_if_leap();
169 assert_eq!(result_invalid, None);
170}
171
172impl AsULE for MonthCode {
173 type ULE = TinyStr4;
174 fn to_unaligned(self) -> TinyStr4 {
175 self.0
176 }
177 fn from_unaligned(u: TinyStr4) -> Self {
178 Self(u)
179 }
180}
181
182impl<'a> ZeroMapKV<'a> for MonthCode {
183 type Container = zerovec::ZeroVec<'a, MonthCode>;
184 type Slice = zerovec::ZeroSlice<MonthCode>;
185 type GetType = <MonthCode as AsULE>::ULE;
186 type OwnedType = MonthCode;
187}
188
189impl fmt::Display for MonthCode {
190 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
191 write!(f, "{}", self.0)
192 }
193}
194
195impl From<TinyStr4> for MonthCode {
196 fn from(x: TinyStr4) -> Self {
197 Self(x)
198 }
199}
200impl FromStr for MonthCode {
201 type Err = <TinyStr4 as FromStr>::Err;
202 fn from_str(s: &str) -> Result<Self, Self::Err> {
203 s.parse().map(Self)
204 }
205}
206
207#[derive(Copy, Clone, Debug, PartialEq)]
209#[allow(clippy::exhaustive_structs)] pub struct FormattableMonth {
211 pub ordinal: u32,
216
217 pub code: MonthCode,
224}
225
226#[derive(Copy, Clone, Debug, PartialEq)]
230#[allow(clippy::exhaustive_structs)] pub struct DayOfYearInfo {
232 pub day_of_year: u16,
234 pub days_in_year: u16,
236 pub prev_year: FormattableYear,
238 pub days_in_prev_year: u16,
240 pub next_year: FormattableYear,
242}
243
244#[allow(clippy::exhaustive_structs)] #[derive(Clone, Copy, Debug, PartialEq)]
247pub struct DayOfMonth(pub u32);
248
249#[derive(Clone, Copy, Debug, PartialEq)]
251#[allow(clippy::exhaustive_structs)] pub struct WeekOfMonth(pub u32);
253
254#[derive(Clone, Copy, Debug, PartialEq)]
256#[allow(clippy::exhaustive_structs)] pub struct WeekOfYear(pub u32);
258
259#[derive(Clone, Copy, Debug, PartialEq)]
261#[allow(clippy::exhaustive_structs)] pub struct DayOfWeekInMonth(pub u32);
263
264impl From<DayOfMonth> for DayOfWeekInMonth {
265 fn from(day_of_month: DayOfMonth) -> Self {
266 DayOfWeekInMonth(1 + ((day_of_month.0 - 1) / 7))
267 }
268}
269
270#[test]
271fn test_day_of_week_in_month() {
272 assert_eq!(DayOfWeekInMonth::from(DayOfMonth(1)).0, 1);
273 assert_eq!(DayOfWeekInMonth::from(DayOfMonth(7)).0, 1);
274 assert_eq!(DayOfWeekInMonth::from(DayOfMonth(8)).0, 2);
275}
276
277macro_rules! dt_unit {
282 ($name:ident, $storage:ident, $value:expr, $docs:expr) => {
283 #[doc=$docs]
284 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
285 pub struct $name($storage);
286
287 impl $name {
288 pub const fn number(self) -> $storage {
290 self.0
291 }
292
293 pub const fn zero() -> $name {
295 Self(0)
296 }
297 }
298
299 impl FromStr for $name {
300 type Err = CalendarError;
301
302 fn from_str(input: &str) -> Result<Self, Self::Err> {
303 let val: $storage = input.parse()?;
304 if val > $value {
305 Err(CalendarError::Overflow {
306 field: "$name",
307 max: $value,
308 })
309 } else {
310 Ok(Self(val))
311 }
312 }
313 }
314
315 impl TryFrom<$storage> for $name {
316 type Error = CalendarError;
317
318 fn try_from(input: $storage) -> Result<Self, Self::Error> {
319 if input > $value {
320 Err(CalendarError::Overflow {
321 field: "$name",
322 max: $value,
323 })
324 } else {
325 Ok(Self(input))
326 }
327 }
328 }
329
330 impl TryFrom<usize> for $name {
331 type Error = CalendarError;
332
333 fn try_from(input: usize) -> Result<Self, Self::Error> {
334 if input > $value {
335 Err(CalendarError::Overflow {
336 field: "$name",
337 max: $value,
338 })
339 } else {
340 Ok(Self(input as $storage))
341 }
342 }
343 }
344
345 impl From<$name> for $storage {
346 fn from(input: $name) -> Self {
347 input.0
348 }
349 }
350
351 impl From<$name> for usize {
352 fn from(input: $name) -> Self {
353 input.0 as Self
354 }
355 }
356
357 impl $name {
358 pub fn try_add(self, other: $storage) -> Option<Self> {
362 let sum = self.0.saturating_add(other);
363 if sum > $value {
364 None
365 } else {
366 Some(Self(sum))
367 }
368 }
369
370 pub fn try_sub(self, other: $storage) -> Option<Self> {
374 self.0.checked_sub(other).map(Self)
375 }
376 }
377 };
378}
379
380dt_unit!(
381 IsoHour,
382 u8,
383 24,
384 "An ISO-8601 hour component, for use with ISO calendars.
385
386Must be within inclusive bounds `[0, 24]`. The value could be equal to 24 to
387denote the end of a day, with the writing 24:00:00. It corresponds to the same
388time as the next day at 00:00:00."
389);
390
391dt_unit!(
392 IsoMinute,
393 u8,
394 60,
395 "An ISO-8601 minute component, for use with ISO calendars.
396
397Must be within inclusive bounds `[0, 60]`. The value could be equal to 60 to
398denote the end of an hour, with the writing 12:60:00. This example corresponds
399to the same time as 13:00:00. This is an extension to ISO 8601."
400);
401
402dt_unit!(
403 IsoSecond,
404 u8,
405 61,
406 "An ISO-8601 second component, for use with ISO calendars.
407
408Must be within inclusive bounds `[0, 61]`. `60` accommodates for leap seconds.
409
410The value could also be equal to 60 or 61, to indicate the end of a leap second,
411with the writing `23:59:61.000000000Z` or `23:59:60.000000000Z`. These examples,
412if used with this goal, would correspond to the same time as the next day, at
413time `00:00:00.000000000Z`. This is an extension to ISO 8601."
414);
415
416dt_unit!(
417 NanoSecond,
418 u32,
419 999_999_999,
420 "A fractional second component, stored as nanoseconds.
421
422Must be within inclusive bounds `[0, 999_999_999]`."
423);
424
425#[test]
426fn test_iso_hour_arithmetic() {
427 const HOUR_MAX: u8 = 24;
428 const HOUR_VALUE: u8 = 5;
429 let hour = IsoHour(HOUR_VALUE);
430
431 assert_eq!(
433 hour.try_add(HOUR_VALUE - 1),
434 Some(IsoHour(HOUR_VALUE + (HOUR_VALUE - 1)))
435 );
436 assert_eq!(
437 hour.try_sub(HOUR_VALUE - 1),
438 Some(IsoHour(HOUR_VALUE - (HOUR_VALUE - 1)))
439 );
440
441 assert_eq!(hour.try_add(HOUR_MAX - HOUR_VALUE), Some(IsoHour(HOUR_MAX)));
443 assert_eq!(hour.try_sub(HOUR_VALUE), Some(IsoHour(0)));
444
445 assert_eq!(hour.try_add(1 + HOUR_MAX - HOUR_VALUE), None);
447 assert_eq!(hour.try_sub(1 + HOUR_VALUE), None);
448}
449
450#[test]
451fn test_iso_minute_arithmetic() {
452 const MINUTE_MAX: u8 = 60;
453 const MINUTE_VALUE: u8 = 5;
454 let minute = IsoMinute(MINUTE_VALUE);
455
456 assert_eq!(
458 minute.try_add(MINUTE_VALUE - 1),
459 Some(IsoMinute(MINUTE_VALUE + (MINUTE_VALUE - 1)))
460 );
461 assert_eq!(
462 minute.try_sub(MINUTE_VALUE - 1),
463 Some(IsoMinute(MINUTE_VALUE - (MINUTE_VALUE - 1)))
464 );
465
466 assert_eq!(
468 minute.try_add(MINUTE_MAX - MINUTE_VALUE),
469 Some(IsoMinute(MINUTE_MAX))
470 );
471 assert_eq!(minute.try_sub(MINUTE_VALUE), Some(IsoMinute(0)));
472
473 assert_eq!(minute.try_add(1 + MINUTE_MAX - MINUTE_VALUE), None);
475 assert_eq!(minute.try_sub(1 + MINUTE_VALUE), None);
476}
477
478#[test]
479fn test_iso_second_arithmetic() {
480 const SECOND_MAX: u8 = 61;
481 const SECOND_VALUE: u8 = 5;
482 let second = IsoSecond(SECOND_VALUE);
483
484 assert_eq!(
486 second.try_add(SECOND_VALUE - 1),
487 Some(IsoSecond(SECOND_VALUE + (SECOND_VALUE - 1)))
488 );
489 assert_eq!(
490 second.try_sub(SECOND_VALUE - 1),
491 Some(IsoSecond(SECOND_VALUE - (SECOND_VALUE - 1)))
492 );
493
494 assert_eq!(
496 second.try_add(SECOND_MAX - SECOND_VALUE),
497 Some(IsoSecond(SECOND_MAX))
498 );
499 assert_eq!(second.try_sub(SECOND_VALUE), Some(IsoSecond(0)));
500
501 assert_eq!(second.try_add(1 + SECOND_MAX - SECOND_VALUE), None);
503 assert_eq!(second.try_sub(1 + SECOND_VALUE), None);
504}
505
506#[test]
507fn test_iso_nano_second_arithmetic() {
508 const NANO_SECOND_MAX: u32 = 999_999_999;
509 const NANO_SECOND_VALUE: u32 = 5;
510 let nano_second = NanoSecond(NANO_SECOND_VALUE);
511
512 assert_eq!(
514 nano_second.try_add(NANO_SECOND_VALUE - 1),
515 Some(NanoSecond(NANO_SECOND_VALUE + (NANO_SECOND_VALUE - 1)))
516 );
517 assert_eq!(
518 nano_second.try_sub(NANO_SECOND_VALUE - 1),
519 Some(NanoSecond(NANO_SECOND_VALUE - (NANO_SECOND_VALUE - 1)))
520 );
521
522 assert_eq!(
524 nano_second.try_add(NANO_SECOND_MAX - NANO_SECOND_VALUE),
525 Some(NanoSecond(NANO_SECOND_MAX))
526 );
527 assert_eq!(nano_second.try_sub(NANO_SECOND_VALUE), Some(NanoSecond(0)));
528
529 assert_eq!(
531 nano_second.try_add(1 + NANO_SECOND_MAX - NANO_SECOND_VALUE),
532 None
533 );
534 assert_eq!(nano_second.try_sub(1 + NANO_SECOND_VALUE), None);
535}
536
537#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
539#[allow(clippy::exhaustive_structs)] pub struct Time {
541 pub hour: IsoHour,
543
544 pub minute: IsoMinute,
546
547 pub second: IsoSecond,
549
550 pub nanosecond: NanoSecond,
552}
553
554impl Time {
555 pub const fn new(
557 hour: IsoHour,
558 minute: IsoMinute,
559 second: IsoSecond,
560 nanosecond: NanoSecond,
561 ) -> Self {
562 Self {
563 hour,
564 minute,
565 second,
566 nanosecond,
567 }
568 }
569
570 pub const fn midnight() -> Self {
572 Self {
573 hour: IsoHour::zero(),
574 minute: IsoMinute::zero(),
575 second: IsoSecond::zero(),
576 nanosecond: NanoSecond::zero(),
577 }
578 }
579
580 pub fn try_new(
582 hour: u8,
583 minute: u8,
584 second: u8,
585 nanosecond: u32,
586 ) -> Result<Self, CalendarError> {
587 Ok(Self {
588 hour: hour.try_into()?,
589 minute: minute.try_into()?,
590 second: second.try_into()?,
591 nanosecond: nanosecond.try_into()?,
592 })
593 }
594
595 pub(crate) fn from_minute_with_remainder_days(minute: i32) -> (Time, i32) {
598 let (extra_days, minute_in_day) = (minute.div_euclid(1440), minute.rem_euclid(1440));
599 let (hours, minutes) = (minute_in_day / 60, minute_in_day % 60);
600 #[allow(clippy::unwrap_used)] (
602 Self {
603 hour: (hours as u8).try_into().unwrap(),
604 minute: (minutes as u8).try_into().unwrap(),
605 second: IsoSecond::zero(),
606 nanosecond: NanoSecond::zero(),
607 },
608 extra_days,
609 )
610 }
611}
612
613#[test]
614fn test_from_minute_with_remainder_days() {
615 #[derive(Debug)]
616 struct TestCase {
617 minute: i32,
618 expected_time: Time,
619 expected_remainder: i32,
620 }
621 let zero_time = Time::new(
622 IsoHour::zero(),
623 IsoMinute::zero(),
624 IsoSecond::zero(),
625 NanoSecond::zero(),
626 );
627 let first_minute_in_day = Time::new(
628 IsoHour::zero(),
629 IsoMinute::try_from(1u8).unwrap(),
630 IsoSecond::zero(),
631 NanoSecond::zero(),
632 );
633 let last_minute_in_day = Time::new(
634 IsoHour::try_from(23u8).unwrap(),
635 IsoMinute::try_from(59u8).unwrap(),
636 IsoSecond::zero(),
637 NanoSecond::zero(),
638 );
639 let cases = [
640 TestCase {
641 minute: 0,
642 expected_time: zero_time,
643 expected_remainder: 0,
644 },
645 TestCase {
646 minute: 30,
647 expected_time: Time::new(
648 IsoHour::zero(),
649 IsoMinute::try_from(30u8).unwrap(),
650 IsoSecond::zero(),
651 NanoSecond::zero(),
652 ),
653 expected_remainder: 0,
654 },
655 TestCase {
656 minute: 60,
657 expected_time: Time::new(
658 IsoHour::try_from(1u8).unwrap(),
659 IsoMinute::zero(),
660 IsoSecond::zero(),
661 NanoSecond::zero(),
662 ),
663 expected_remainder: 0,
664 },
665 TestCase {
666 minute: 90,
667 expected_time: Time::new(
668 IsoHour::try_from(1u8).unwrap(),
669 IsoMinute::try_from(30u8).unwrap(),
670 IsoSecond::zero(),
671 NanoSecond::zero(),
672 ),
673 expected_remainder: 0,
674 },
675 TestCase {
676 minute: 1439,
677 expected_time: last_minute_in_day,
678 expected_remainder: 0,
679 },
680 TestCase {
681 minute: 1440,
682 expected_time: Time::new(
683 IsoHour::zero(),
684 IsoMinute::zero(),
685 IsoSecond::zero(),
686 NanoSecond::zero(),
687 ),
688 expected_remainder: 1,
689 },
690 TestCase {
691 minute: 1441,
692 expected_time: first_minute_in_day,
693 expected_remainder: 1,
694 },
695 TestCase {
696 minute: i32::MAX,
697 expected_time: Time::new(
698 IsoHour::try_from(2u8).unwrap(),
699 IsoMinute::try_from(7u8).unwrap(),
700 IsoSecond::zero(),
701 NanoSecond::zero(),
702 ),
703 expected_remainder: 1491308,
704 },
705 TestCase {
706 minute: -1,
707 expected_time: last_minute_in_day,
708 expected_remainder: -1,
709 },
710 TestCase {
711 minute: -1439,
712 expected_time: first_minute_in_day,
713 expected_remainder: -1,
714 },
715 TestCase {
716 minute: -1440,
717 expected_time: zero_time,
718 expected_remainder: -1,
719 },
720 TestCase {
721 minute: -1441,
722 expected_time: last_minute_in_day,
723 expected_remainder: -2,
724 },
725 TestCase {
726 minute: i32::MIN,
727 expected_time: Time::new(
728 IsoHour::try_from(21u8).unwrap(),
729 IsoMinute::try_from(52u8).unwrap(),
730 IsoSecond::zero(),
731 NanoSecond::zero(),
732 ),
733 expected_remainder: -1491309,
734 },
735 ];
736 for cas in cases {
737 let (actual_time, actual_remainder) = Time::from_minute_with_remainder_days(cas.minute);
738 assert_eq!(actual_time, cas.expected_time, "{cas:?}");
739 assert_eq!(actual_remainder, cas.expected_remainder, "{cas:?}");
740 }
741}
742
743#[derive(Clone, Copy, Debug, PartialEq, Eq)]
756#[allow(missing_docs)] #[repr(i8)]
758#[cfg_attr(
759 feature = "datagen",
760 derive(serde::Serialize, databake::Bake),
761 databake(path = icu_calendar::types),
762)]
763#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
764#[allow(clippy::exhaustive_enums)] pub enum IsoWeekday {
766 Monday = 1,
767 Tuesday,
768 Wednesday,
769 Thursday,
770 Friday,
771 Saturday,
772 Sunday,
773}
774
775impl From<usize> for IsoWeekday {
776 fn from(input: usize) -> Self {
790 use IsoWeekday::*;
791 match input % 7 {
792 0 => Sunday,
793 1 => Monday,
794 2 => Tuesday,
795 3 => Wednesday,
796 4 => Thursday,
797 5 => Friday,
798 6 => Saturday,
799 _ => unreachable!(),
800 }
801 }
802}
803
804impl IsoWeekday {
805 pub(crate) fn next_day(self) -> IsoWeekday {
807 use IsoWeekday::*;
808 match self {
809 Monday => Tuesday,
810 Tuesday => Wednesday,
811 Wednesday => Thursday,
812 Thursday => Friday,
813 Friday => Saturday,
814 Saturday => Sunday,
815 Sunday => Monday,
816 }
817 }
818}