rkg_utils/header/date.rs
1use crate::byte_handler::{ByteHandlerError, FromByteHandler};
2
3/// Errors that can occur while constructing a [`Date`].
4#[derive(thiserror::Error, Debug)]
5pub enum DateError {
6 /// The year is outside the valid range (2000–2035).
7 #[error("Year is invalid")]
8 YearInvalid,
9 /// The month is outside the valid range (1–12).
10 #[error("Month is invalid")]
11 MonthInvalid,
12 /// The day is zero, or exceeds the maximum number of days for the given month and year.
13 #[error("Day is invalid")]
14 DayInvalid,
15 /// A `ByteHandler` operation failed.
16 #[error("ByteHandler Error: {0}")]
17 ByteHandlerError(#[from] ByteHandlerError),
18}
19
20/// All valid dates on the Wii. Year is a range between 0 and 35, symbolizing 2000 and 2035
21/// respectively. Leap years are accounted for when validating February day counts.
22#[derive(Debug)]
23pub struct Date {
24 /// Year offset from 2000 (0–35), representing 2000–2035.
25 year: u8,
26 /// Month (1–12).
27 month: u8,
28 /// Day of the month.
29 day: u8,
30}
31
32impl Date {
33 /// Creates a new [`Date`] from a full year, month, and day.
34 ///
35 /// # Arguments
36 ///
37 /// * `year` - The full calendar year (2000–2035).
38 /// * `month` - The month (1–12).
39 /// * `day` - The day of the month, validated against the given month and year.
40 ///
41 /// # Errors
42 ///
43 /// Returns [`DateError::YearInvalid`] if `year` is outside the range 2000–2035.
44 /// Returns [`DateError::MonthInvalid`] if `month` is outside the range 1–12.
45 /// Returns [`DateError::DayInvalid`] if `day` exceeds the maximum for the given
46 /// month, accounting for leap years in February.
47 pub fn new(year: u16, month: u8, day: u8) -> Result<Self, DateError> {
48 let year = (year - 2000) as u8;
49
50 if year > 35 {
51 return Err(DateError::YearInvalid);
52 }
53
54 match month {
55 1 | 3 | 5 | 7 | 8 | 10 | 12 if day > 31 => Err(DateError::DayInvalid),
56 4 | 6 | 9 | 11 if day > 30 => Err(DateError::DayInvalid),
57 2 if year.is_multiple_of(4) && day > 29 => Err(DateError::DayInvalid),
58 2 if !year.is_multiple_of(4) && day > 28 => Err(DateError::DayInvalid),
59 1..=12 => Ok(Self { year, month, day }),
60 _ => Err(DateError::MonthInvalid),
61 }
62 }
63
64 /// Returns the full calendar year (2000–2035).
65 ///
66 /// The year is stored internally as an offset from 2000; this method adds
67 /// 2000 to produce the full four-digit year.
68 pub fn year(&self) -> u16 {
69 (self.year as u16) + 2000
70 }
71
72 /// Returns the month (1–12).
73 pub fn month(&self) -> u8 {
74 self.month
75 }
76
77 /// Returns the day of the month.
78 pub fn day(&self) -> u8 {
79 self.day
80 }
81}
82
83/// Deserializes a [`Date`] from 3 bytes at header offset `0x09..=0x0B`.
84///
85/// The bytes are packed as follows:
86/// ```text
87/// Byte 1: XXXXYYYY
88/// Byte 2: YYYMMMMD
89/// Byte 3: DDDDXXXX
90/// ```
91/// where `Y` = year bits, `M` = month bits, and `D` = day bits.
92impl FromByteHandler for Date {
93 type Err = DateError;
94 fn from_byte_handler<T>(handler: T) -> Result<Self, Self::Err>
95 where
96 T: TryInto<crate::byte_handler::ByteHandler>,
97 Self::Err: From<T::Error>,
98 {
99 let mut handler = handler.try_into()?;
100
101 handler.shift_right(4);
102 let year = ((handler.copy_byte(1) >> 1) as u16) + 2000;
103 let day = handler.copy_byte(2) & 0x1F;
104 handler.shift_right(5);
105 let month = handler.copy_byte(2) & 0x0F;
106
107 Self::new(year, month, day)
108 }
109}
110
111/// Formats [`Date`] as YYYY-MM-DD
112impl std::fmt::Display for Date {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 write!(f, "20{:02}-{:02}-{:02}", self.year, self.month, self.day)
115 }
116}
117
118/// Two [`Date`] values are equal if their year, month, and day are all identical.
119impl PartialEq for Date {
120 fn eq(&self, other: &Self) -> bool {
121 self.day == other.day && self.month == other.month && self.year == other.year
122 }
123}