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