Skip to main content

celestial_time/
parsing.rs

1use crate::{JulianDate, TimeError, TimeResult};
2
3#[derive(Debug, Clone)]
4pub struct ParsedDateTime {
5    pub year: i32,
6    pub month: u8,
7    pub day: u8,
8    pub hour: u8,
9    pub minute: u8,
10    pub second: f64,
11}
12
13impl ParsedDateTime {
14    pub fn to_julian_date(&self) -> JulianDate {
15        JulianDate::from_calendar(
16            self.year,
17            self.month,
18            self.day,
19            self.hour,
20            self.minute,
21            self.second,
22        )
23    }
24}
25
26pub fn parse_iso8601(s: &str) -> TimeResult<ParsedDateTime> {
27    let s = s.trim();
28
29    const MAX_ISO8601_LENGTH: usize = 32;
30    if s.len() > MAX_ISO8601_LENGTH {
31        return Err(TimeError::ParseError("Input too long".to_string()));
32    }
33
34    let s = s.strip_suffix('Z').unwrap_or(s);
35
36    let separator_pos = s.find('T').or_else(|| s.find(' ')).ok_or_else(|| {
37        TimeError::ParseError(format!(
38            "Invalid datetime format: '{}'. Expected YYYY-MM-DDTHH:MM:SS",
39            s
40        ))
41    })?;
42
43    let (date_part, time_part_with_sep) = s.split_at(separator_pos);
44    let time_part = &time_part_with_sep[1..];
45
46    let date_components: Vec<&str> = date_part.split('-').collect();
47    if date_components.len() != 3 {
48        return Err(TimeError::ParseError(format!(
49            "Invalid date format: '{}'. Expected YYYY-MM-DD",
50            date_part
51        )));
52    }
53
54    let year = if date_components[0].len() == 4 {
55        let bytes = date_components[0].as_bytes();
56        if bytes.iter().all(|&b| b.is_ascii_digit()) {
57            (bytes[0] - b'0') as i32 * 1000
58                + (bytes[1] - b'0') as i32 * 100
59                + (bytes[2] - b'0') as i32 * 10
60                + (bytes[3] - b'0') as i32
61        } else {
62            return Err(TimeError::ParseError(format!(
63                "Invalid year: '{}'",
64                date_components[0]
65            )));
66        }
67    } else {
68        return Err(TimeError::ParseError(format!(
69            "Invalid year format: '{}'",
70            date_components[0]
71        )));
72    };
73
74    let month = match date_components[1].len() {
75        1 => {
76            let b = date_components[1].as_bytes()[0];
77            if b.is_ascii_digit() {
78                b - b'0'
79            } else {
80                return Err(TimeError::ParseError(format!(
81                    "Invalid month: '{}'",
82                    date_components[1]
83                )));
84            }
85        }
86        2 => {
87            let bytes = date_components[1].as_bytes();
88            if bytes.iter().all(|&b| b.is_ascii_digit()) {
89                (bytes[0] - b'0') * 10 + (bytes[1] - b'0')
90            } else {
91                return Err(TimeError::ParseError(format!(
92                    "Invalid month: '{}'",
93                    date_components[1]
94                )));
95            }
96        }
97        _ => {
98            return Err(TimeError::ParseError(format!(
99                "Invalid month format: '{}'",
100                date_components[1]
101            )))
102        }
103    };
104
105    let day = match date_components[2].len() {
106        1 => {
107            let b = date_components[2].as_bytes()[0];
108            if b.is_ascii_digit() {
109                b - b'0'
110            } else {
111                return Err(TimeError::ParseError(format!(
112                    "Invalid day: '{}'",
113                    date_components[2]
114                )));
115            }
116        }
117        2 => {
118            let bytes = date_components[2].as_bytes();
119            if bytes.iter().all(|&b| b.is_ascii_digit()) {
120                (bytes[0] - b'0') * 10 + (bytes[1] - b'0')
121            } else {
122                return Err(TimeError::ParseError(format!(
123                    "Invalid day: '{}'",
124                    date_components[2]
125                )));
126            }
127        }
128        _ => {
129            return Err(TimeError::ParseError(format!(
130                "Invalid day format: '{}'",
131                date_components[2]
132            )))
133        }
134    };
135
136    if !(1..=12).contains(&month) {
137        return Err(TimeError::ParseError(format!(
138            "Month out of range: {}",
139            month
140        )));
141    }
142    if !(1..=31).contains(&day) {
143        return Err(TimeError::ParseError(format!("Day out of range: {}", day)));
144    }
145
146    let time_components: Vec<&str> = time_part.split(':').collect();
147    if time_components.len() != 3 {
148        return Err(TimeError::ParseError(format!(
149            "Invalid time format: '{}'. Expected HH:MM:SS",
150            time_part
151        )));
152    }
153
154    let hour = match time_components[0].len() {
155        1 => {
156            let b = time_components[0].as_bytes()[0];
157            if b.is_ascii_digit() {
158                b - b'0'
159            } else {
160                return Err(TimeError::ParseError(format!(
161                    "Invalid hour: '{}'",
162                    time_components[0]
163                )));
164            }
165        }
166        2 => {
167            let bytes = time_components[0].as_bytes();
168            if bytes.iter().all(|&b| b.is_ascii_digit()) {
169                (bytes[0] - b'0') * 10 + (bytes[1] - b'0')
170            } else {
171                return Err(TimeError::ParseError(format!(
172                    "Invalid hour: '{}'",
173                    time_components[0]
174                )));
175            }
176        }
177        _ => {
178            return Err(TimeError::ParseError(format!(
179                "Invalid hour format: '{}'",
180                time_components[0]
181            )))
182        }
183    };
184
185    let minute = match time_components[1].len() {
186        1 => {
187            let b = time_components[1].as_bytes()[0];
188            if b.is_ascii_digit() {
189                b - b'0'
190            } else {
191                return Err(TimeError::ParseError(format!(
192                    "Invalid minute: '{}'",
193                    time_components[1]
194                )));
195            }
196        }
197        2 => {
198            let bytes = time_components[1].as_bytes();
199            if bytes.iter().all(|&b| b.is_ascii_digit()) {
200                (bytes[0] - b'0') * 10 + (bytes[1] - b'0')
201            } else {
202                return Err(TimeError::ParseError(format!(
203                    "Invalid minute: '{}'",
204                    time_components[1]
205                )));
206            }
207        }
208        _ => {
209            return Err(TimeError::ParseError(format!(
210                "Invalid minute format: '{}'",
211                time_components[1]
212            )))
213        }
214    };
215
216    let second = time_components[2]
217        .parse::<f64>()
218        .map_err(|_| TimeError::ParseError(format!("Invalid second: '{}'", time_components[2])))?;
219
220    if hour > 23 {
221        return Err(TimeError::ParseError(format!(
222            "Hour out of range: {}",
223            hour
224        )));
225    }
226    if minute > 59 {
227        return Err(TimeError::ParseError(format!(
228            "Minute out of range: {}",
229            minute
230        )));
231    }
232    if second >= 60.0 {
233        return Err(TimeError::ParseError(format!(
234            "Second out of range: {}",
235            second
236        )));
237    }
238
239    Ok(ParsedDateTime {
240        year,
241        month,
242        day,
243        hour,
244        minute,
245        second,
246    })
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_iso8601() {
255        let dt = parse_iso8601("2000-01-01T12:00:00").unwrap();
256        assert_eq!(dt.year, 2000);
257        assert_eq!(dt.month, 1);
258        assert_eq!(dt.day, 1);
259        assert_eq!(dt.hour, 12);
260        assert_eq!(dt.minute, 0);
261        assert_eq!(dt.second, 0.0);
262    }
263
264    #[test]
265    fn test_iso8601_with_fractional_seconds() {
266        let dt = parse_iso8601("2000-01-01T12:00:00.123").unwrap();
267        assert_eq!(dt.second, 0.123);
268    }
269
270    #[test]
271    fn test_iso8601_with_z_suffix() {
272        let dt = parse_iso8601("2000-01-01T12:00:00Z").unwrap();
273        assert_eq!(dt.year, 2000);
274        assert_eq!(dt.hour, 12);
275    }
276
277    #[test]
278    fn test_iso8601_space_separator() {
279        let dt = parse_iso8601("2000-01-01 12:00:00").unwrap();
280        assert_eq!(dt.year, 2000);
281        assert_eq!(dt.hour, 12);
282    }
283
284    #[test]
285    fn test_invalid_format() {
286        assert!(parse_iso8601("not-a-date").is_err());
287        assert!(parse_iso8601("2000-01-01").is_err());
288        assert!(parse_iso8601("12:00:00").is_err());
289    }
290
291    #[test]
292    fn test_invalid_ranges() {
293        assert!(parse_iso8601("2000-13-01T12:00:00").is_err());
294        assert!(parse_iso8601("2000-01-32T12:00:00").is_err());
295        assert!(parse_iso8601("2000-01-01T25:00:00").is_err());
296        assert!(parse_iso8601("2000-01-01T12:60:00").is_err());
297        assert!(parse_iso8601("2000-01-01T12:00:60").is_err());
298    }
299
300    #[test]
301    fn test_to_julian_date() {
302        let dt = parse_iso8601("2000-01-01T12:00:00").unwrap();
303        let jd = dt.to_julian_date();
304        assert_eq!(jd.to_f64(), celestial_core::constants::J2000_JD);
305    }
306
307    #[test]
308    fn test_input_too_long() {
309        let long_input = "2000-01-01T12:00:00.".repeat(10);
310        assert!(parse_iso8601(&long_input).is_err());
311        if let Err(TimeError::ParseError(msg)) = parse_iso8601(&long_input) {
312            assert_eq!(msg, "Input too long");
313        } else {
314            panic!("Expected ParseError with 'Input too long'");
315        }
316    }
317
318    #[test]
319    fn test_invalid_date_component_counts() {
320        assert!(parse_iso8601("2000T12:00:00").is_err());
321        assert!(parse_iso8601("2000-01T12:00:00").is_err());
322        assert!(parse_iso8601("2000-01-01-01T12:00:00").is_err());
323    }
324
325    #[test]
326    fn test_invalid_year_formats() {
327        assert!(parse_iso8601("20a0-01-01T12:00:00").is_err());
328        assert!(parse_iso8601("200-01-01T12:00:00").is_err());
329        assert!(parse_iso8601("20000-01-01T12:00:00").is_err());
330    }
331
332    #[test]
333    fn test_invalid_month_formats() {
334        assert!(parse_iso8601("2000-a-01T12:00:00").is_err());
335        assert!(parse_iso8601("2000-ab-01T12:00:00").is_err());
336        assert!(parse_iso8601("2000-123-01T12:00:00").is_err());
337    }
338
339    #[test]
340    fn test_invalid_day_formats() {
341        assert!(parse_iso8601("2000-01-aT12:00:00").is_err());
342        assert!(parse_iso8601("2000-01-abT12:00:00").is_err());
343        assert!(parse_iso8601("2000-01-123T12:00:00").is_err());
344    }
345
346    #[test]
347    fn test_invalid_time_component_counts() {
348        assert!(parse_iso8601("2000-01-01T12").is_err());
349        assert!(parse_iso8601("2000-01-01T12:00").is_err());
350        assert!(parse_iso8601("2000-01-01T12:00:00:00").is_err());
351    }
352
353    #[test]
354    fn test_invalid_hour_formats() {
355        assert!(parse_iso8601("2000-01-01Ta:00:00").is_err());
356        assert!(parse_iso8601("2000-01-01Tab:00:00").is_err());
357        assert!(parse_iso8601("2000-01-01T123:00:00").is_err());
358    }
359
360    #[test]
361    fn test_invalid_minute_formats() {
362        assert!(parse_iso8601("2000-01-01T12:a:00").is_err());
363        assert!(parse_iso8601("2000-01-01T12:ab:00").is_err());
364        assert!(parse_iso8601("2000-01-01T12:123:00").is_err());
365    }
366
367    #[test]
368    fn test_invalid_second_format() {
369        assert!(parse_iso8601("2000-01-01T12:00:ab").is_err());
370        assert!(parse_iso8601("2000-01-01T12:00:").is_err());
371    }
372
373    #[test]
374    fn test_single_digit_components() {
375        let dt = parse_iso8601("2000-1-1T1:1:1").unwrap();
376        assert_eq!(dt.year, 2000);
377        assert_eq!(dt.month, 1);
378        assert_eq!(dt.day, 1);
379        assert_eq!(dt.hour, 1);
380        assert_eq!(dt.minute, 1);
381        assert_eq!(dt.second, 1.0);
382    }
383
384    #[test]
385    fn test_edge_case_ranges() {
386        assert!(parse_iso8601("2000-00-01T12:00:00").is_err());
387        assert!(parse_iso8601("2000-01-00T12:00:00").is_err());
388        assert!(parse_iso8601("2000-12-31T23:59:59.999").is_ok());
389    }
390
391    #[test]
392    fn test_whitespace_handling() {
393        let dt = parse_iso8601("  2000-01-01T12:00:00  ").unwrap();
394        assert_eq!(dt.year, 2000);
395        assert_eq!(dt.hour, 12);
396    }
397
398    #[test]
399    fn test_z_suffix_with_fractional_seconds() {
400        let dt = parse_iso8601("2000-01-01T12:00:00.123Z").unwrap();
401        assert_eq!(dt.second, 0.123);
402    }
403}