hayro_syntax/object/
date.rs

1use crate::byte_reader::Reader;
2use std::str::FromStr;
3
4/// A date time.
5#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
6pub struct DateTime {
7    /// The year.
8    pub year: u16,
9    /// The year.
10    pub month: u8,
11    /// The day.
12    pub day: u8,
13    /// The hour.
14    pub hour: u8,
15    /// The minute.
16    pub minute: u8,
17    /// The second.
18    pub second: u8,
19    /// The offset in hours from UTC.
20    pub utc_offset_hour: i8,
21    /// The offset in minutes from UTC.
22    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}