1use crate::byte_reader::Reader;
2use core::str::FromStr;
3
4#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
6pub struct DateTime {
7 pub year: u16,
9 pub month: u8,
11 pub day: u8,
13 pub hour: u8,
15 pub minute: u8,
17 pub second: u8,
19 pub utc_offset_hour: i8,
21 pub utc_offset_minute: u8,
23 pub has_timezone: bool,
25}
26
27impl DateTime {
28 pub(crate) fn from_bytes(bytes: &[u8]) -> Option<Self> {
29 let mut reader = Reader::new(bytes);
30
31 reader.forward_tag(b"D:")?;
32
33 let read_num = |reader: &mut Reader<'_>, bytes: u8, min: u16, max: u16| -> Option<u16> {
34 if matches!(reader.peek_byte()?, b'-' | b'+' | b'Z') {
35 return None;
36 }
37
38 let num = u16::from_str(core::str::from_utf8(reader.read_bytes(bytes as usize)?).ok()?)
39 .ok()?;
40
41 if num < min || num > max {
42 return None;
43 }
44
45 Some(num)
46 };
47
48 let year = read_num(&mut reader, 4, 0, 9999)?;
49 let month = read_num(&mut reader, 2, 1, 12)
50 .map(|n| n as u8)
51 .unwrap_or(1);
52 let day = read_num(&mut reader, 2, 1, 31)
53 .map(|n| n as u8)
54 .unwrap_or(1);
55 let hour = read_num(&mut reader, 2, 0, 23)
56 .map(|n| n as u8)
57 .unwrap_or(0);
58 let minute = read_num(&mut reader, 2, 0, 59)
59 .map(|n| n as u8)
60 .unwrap_or(0);
61 let second = read_num(&mut reader, 2, 0, 59)
62 .map(|n| n as u8)
63 .unwrap_or(0);
64
65 let (utc_offset_hour, utc_offset_minute, has_timezone) = if !reader.at_end() {
66 let multiplier = match reader.read_byte()? {
67 b'-' => -1,
68 _ => 1, };
70
71 let hour = multiplier
72 * read_num(&mut reader, 2, 0, 23)
73 .map(|n| n as i8)
74 .unwrap_or(0);
75 reader.forward_tag(b"\'");
76 let minute = read_num(&mut reader, 2, 0, 59)
77 .map(|n| n as u8)
78 .unwrap_or(0);
79
80 (hour, minute, true)
81 } else {
82 (0, 0, false)
83 };
84
85 Some(Self {
86 year,
87 month,
88 day,
89 hour,
90 minute,
91 second,
92 utc_offset_hour,
93 utc_offset_minute,
94 has_timezone,
95 })
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::DateTime;
102
103 #[allow(clippy::too_many_arguments)]
104 fn dt(
105 year: u16,
106 month: u8,
107 day: u8,
108 hour: u8,
109 minute: u8,
110 second: u8,
111 utc_hour: i8,
112 utc_minute: u8,
113 ) -> DateTime {
114 DateTime {
115 year,
116 month,
117 day,
118 hour,
119 minute,
120 second,
121 utc_offset_hour: utc_hour,
122 utc_offset_minute: utc_minute,
123 has_timezone: false,
124 }
125 }
126
127 #[allow(clippy::too_many_arguments)]
129 fn dt_tz(
130 year: u16,
131 month: u8,
132 day: u8,
133 hour: u8,
134 minute: u8,
135 second: u8,
136 utc_hour: i8,
137 utc_minute: u8,
138 ) -> DateTime {
139 DateTime {
140 year,
141 month,
142 day,
143 hour,
144 minute,
145 second,
146 utc_offset_hour: utc_hour,
147 utc_offset_minute: utc_minute,
148 has_timezone: true,
149 }
150 }
151
152 fn parse(str: &str) -> DateTime {
153 DateTime::from_bytes(str.as_bytes()).unwrap()
154 }
155
156 #[test]
157 fn year_only_defaults() {
158 assert_eq!(parse("D:2023"), dt(2023, 1, 1, 0, 0, 0, 0, 0));
159 }
160
161 #[test]
162 fn year_month_defaults() {
163 assert_eq!(parse("D:202312"), dt(2023, 12, 1, 0, 0, 0, 0, 0));
164 }
165
166 #[test]
167 fn year_month_day_defaults() {
168 assert_eq!(parse("D:20231225"), dt(2023, 12, 25, 0, 0, 0, 0, 0));
169 }
170
171 #[test]
172 fn ymdh() {
173 assert_eq!(parse("D:2023122514"), dt(2023, 12, 25, 14, 0, 0, 0, 0));
174 }
175
176 #[test]
177 fn ymdhm() {
178 assert_eq!(parse("D:202312251430"), dt(2023, 12, 25, 14, 30, 0, 0, 0));
179 }
180
181 #[test]
182 fn full_local_time() {
183 assert_eq!(
184 parse("D:20231225143015"),
185 dt(2023, 12, 25, 14, 30, 15, 0, 0)
186 );
187 }
188
189 #[test]
190 fn example_from_spec() {
191 assert_eq!(
192 parse("D:199812231952-08'00"),
193 dt_tz(1998, 12, 23, 19, 52, 0, -8, 0)
194 );
195 }
196
197 #[test]
198 fn positive_offset_with_minutes() {
199 assert_eq!(
200 parse("D:20230701120000+05'30"),
201 dt_tz(2023, 7, 1, 12, 0, 0, 5, 30)
202 );
203 }
204
205 #[test]
206 fn utc_z() {
207 assert_eq!(
208 parse("D:20230701120000Z"),
209 dt_tz(2023, 7, 1, 12, 0, 0, 0, 0)
210 );
211 }
212
213 #[test]
214 fn utc_z_with_zero_offsets() {
215 assert_eq!(
216 parse("D:20230701120000Z00'00"),
217 dt_tz(2023, 7, 1, 12, 0, 0, 0, 0)
218 );
219 }
220
221 #[test]
222 fn negative_offset_with_minutes() {
223 assert_eq!(
224 parse("D:20230701120000-03'15"),
225 dt_tz(2023, 7, 1, 12, 0, 0, -3, 15)
226 );
227 }
228
229 #[test]
230 fn leap_year() {
231 assert_eq!(
232 parse("D:20000229010203+01'00"),
233 dt_tz(2000, 2, 29, 1, 2, 3, 1, 0)
234 );
235 }
236
237 #[test]
238 fn max_values() {
239 assert_eq!(
240 parse("D:99991231235959+14'00"),
241 dt_tz(9999, 12, 31, 23, 59, 59, 14, 0)
242 );
243 }
244
245 #[test]
246 fn min_values() {
247 assert_eq!(
248 parse("D:00000101000000+00'00"),
249 dt_tz(0, 1, 1, 0, 0, 0, 0, 0)
250 );
251 }
252
253 #[test]
254 fn offset_hour_only() {
255 assert_eq!(
256 parse("D:202307011200+02"),
257 dt_tz(2023, 7, 1, 12, 0, 0, 2, 0)
258 );
259 }
260
261 #[test]
262 fn offset_negative_zero_hour() {
263 assert_eq!(
264 parse("D:202307011200-00'45"),
265 dt_tz(2023, 7, 1, 12, 0, 0, 0, 45)
266 );
267 }
268}