celestial_time/
parsing.rs1use 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}