cftime_rs/
parser.rs

1//! Module related to parsing the date units
2//! Create a `ParsedDatetime` from units
3
4use crate::{calendars::Calendar, duration::CFDuration};
5
6#[derive(Debug, PartialEq)]
7pub enum Unit {
8    Year,
9    Month,
10    Day,
11    Hour,
12    Minute,
13    Second,
14    Millisecond,
15    Microsecond,
16    Nanosecond,
17}
18
19impl Unit {
20    pub fn to_duration(&self, calendar: Calendar) -> CFDuration {
21        match self {
22            Unit::Year => CFDuration::from_years(1, calendar),
23            Unit::Month => CFDuration::from_months(1, calendar),
24            Unit::Day => CFDuration::from_days(1, calendar),
25            Unit::Hour => CFDuration::from_hours(1, calendar),
26            Unit::Minute => CFDuration::from_minutes(1, calendar),
27            Unit::Second => CFDuration::from_seconds(1, calendar),
28            Unit::Millisecond => CFDuration::from_milliseconds(1, calendar),
29            Unit::Microsecond => CFDuration::from_microseconds(1, calendar),
30            Unit::Nanosecond => CFDuration::from_nanoseconds(1, calendar),
31        }
32    }
33}
34#[derive(Debug)]
35pub struct ParsedDatetime {
36    pub ymd: (i64, u8, u8),
37    pub hms: Option<(u8, u8, f32)>,
38    pub tz: Option<(i8, u8)>,
39    pub nanosecond: Option<i64>,
40}
41#[derive(Debug)]
42pub struct ParsedCFTime {
43    pub unit: Unit,
44    pub datetime: ParsedDatetime,
45}
46pub fn parse_cf_time(unit: &str) -> Result<ParsedCFTime, crate::errors::Error> {
47    let mut matches: Vec<&str> = unit.split(' ').collect();
48    // Remove empty strings
49    matches.retain(|&s| !s.trim().is_empty());
50    if matches.len() < 3 {
51        return Err(crate::errors::Error::UnitParserError(unit.to_string()));
52    }
53
54    let duration_unit = match matches[0] {
55        "common_years" | "common_year" => Unit::Year,
56        "months" | "month" => Unit::Month,
57        "days" | "day" | "d" => Unit::Day,
58        "hours" | "hour" | "hrs" | "hr" | "h" => Unit::Hour,
59        "minutes" | "minute" | "mins" | "min" => Unit::Minute,
60        "seconds" | "second" | "secs" | "sec" | "s" => Unit::Second,
61        "milliseconds" | "millisecond" | "millisecs" | "millisec" | "msecs" | "msec" | "ms" => {
62            Unit::Millisecond
63        }
64        "microseconds" | "microsecond" | "microsecs" | "microsec" => Unit::Microsecond,
65        _ => {
66            return Err(crate::errors::Error::UnitParserError(
67                format!("Invalid duration unit '{}' in '{unit}'", matches[0]).to_string(),
68            ))
69        }
70    };
71
72    if matches[1] != "since" {
73        return Err(crate::errors::Error::UnitParserError(
74            format!("Expected 'since' found : '{}'", matches[1]).to_string(),
75        ));
76    }
77
78    let date: Vec<&str> = matches[2].split('-').collect();
79    if date.len() != 3 {
80        return Err(crate::errors::Error::UnitParserError(
81            format!("Invalid date: {unit}").to_string(),
82        ));
83    }
84    let year = date[0].parse::<i64>()?;
85    let month = date[1].parse::<u8>()?;
86    let day = date[2].parse::<u8>()?;
87
88    if matches.len() <= 3 {
89        return Ok(ParsedCFTime {
90            unit: duration_unit,
91            datetime: ParsedDatetime {
92                ymd: (year, month, day),
93                hms: None,
94                tz: None,
95                nanosecond: None,
96            },
97        });
98    }
99
100    let time: Vec<&str> = matches[3].split(':').collect();
101    if time.len() != 3 {
102        return Err(crate::errors::Error::UnitParserError(
103            format!("Invalid time '{}' in '{unit}'", matches[3]).to_string(),
104        ));
105    }
106    let hour = time[0].parse::<u8>()?;
107    let minute = time[1].parse::<u8>()?;
108    let second = time[2].parse::<f32>()?;
109
110    if matches.len() <= 4 {
111        return Ok(ParsedCFTime {
112            unit: duration_unit,
113            datetime: ParsedDatetime {
114                ymd: (year, month, day),
115                hms: Some((hour, minute, second)),
116                tz: None,
117                nanosecond: None,
118            },
119        });
120    }
121
122    let tz: Vec<&str> = matches[4].split(':').collect();
123    if tz.len() > 2 || tz.len() <= 0 {
124        return Err(crate::errors::Error::UnitParserError(
125            format!("Invalid time '{}' in '{unit}'", matches[4]).to_string(),
126        ));
127    }
128    let mut tzhour = 0;
129    let mut tzminute = 0;
130    if tz.len() == 1 {
131        tzhour = tz[0].parse::<i8>()?;
132        tzminute = 0;
133    } else if tz.len() == 2 {
134        tzhour = tz[0].parse::<i8>()?;
135        tzminute = tz[1].parse::<u8>()?;
136    }
137    Ok(ParsedCFTime {
138        unit: duration_unit,
139        datetime: ParsedDatetime {
140            ymd: (year, month, day),
141            hms: Some((hour, minute, second)),
142            tz: Some((tzhour, tzminute)),
143            nanosecond: None,
144        },
145    })
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_valid_duration_units() {
154        // Test valid duration units
155        let units = vec![
156            ("common_years since 2023-01-01", Unit::Year),
157            ("months since 2023-01-01", Unit::Month),
158            ("day since 2023-01-01", Unit::Day),
159            // Add more valid units here
160        ];
161
162        for (input, expected_unit) in units {
163            let result = parse_cf_time(input).unwrap();
164            assert!(result.unit == expected_unit);
165            assert_eq!(result.datetime.ymd, (2023, 1, 1));
166            assert_eq!(result.datetime.hms, None);
167            assert_eq!(result.datetime.tz, None);
168            assert_eq!(result.datetime.nanosecond, None);
169        }
170    }
171
172    #[test]
173    fn test_valid_date_time_units() {
174        // Test valid date and time units with different combinations
175        let units = vec![
176            // From CF conventions
177            (
178                "seconds since 1992-10-8 15:15:42.5 -6:00",
179                ParsedCFTime {
180                    unit: Unit::Second,
181                    datetime: ParsedDatetime {
182                        ymd: (1992, 10, 8),
183                        hms: Some((15, 15, 42.5)),
184                        tz: Some((-6, 0)),
185                        nanosecond: None,
186                    },
187                },
188            ),
189            // Date, no time, no timezone
190            (
191                "seconds since 1992-10-08",
192                ParsedCFTime {
193                    unit: Unit::Second,
194                    datetime: ParsedDatetime {
195                        ymd: (1992, 10, 8),
196                        hms: None,
197                        tz: None,
198                        nanosecond: None,
199                    },
200                },
201            ),
202            (
203                "minutes since 2000-01-01",
204                ParsedCFTime {
205                    unit: Unit::Minute,
206                    datetime: ParsedDatetime {
207                        ymd: (2000, 1, 1),
208                        hms: None,
209                        tz: None,
210                        nanosecond: None,
211                    },
212                },
213            ),
214            (
215                "hour since 1985-12-31",
216                ParsedCFTime {
217                    unit: Unit::Hour,
218                    datetime: ParsedDatetime {
219                        ymd: (1985, 12, 31),
220                        hms: None,
221                        tz: None,
222                        nanosecond: None,
223                    },
224                },
225            ),
226            // Date and time, no timezone
227            (
228                "seconds since 2022-11-30 10:15:20",
229                ParsedCFTime {
230                    unit: Unit::Second,
231                    datetime: ParsedDatetime {
232                        ymd: (2022, 11, 30),
233                        hms: Some((10, 15, 20.0)),
234                        tz: None,
235                        nanosecond: None,
236                    },
237                },
238            ),
239            (
240                "minutes since 2010-05-15 05:30:00",
241                ParsedCFTime {
242                    unit: Unit::Minute,
243                    datetime: ParsedDatetime {
244                        ymd: (2010, 5, 15),
245                        hms: Some((5, 30, 0.0)),
246                        tz: None,
247                        nanosecond: None,
248                    },
249                },
250            ),
251            (
252                "hour since 1999-03-20 12:00:01",
253                ParsedCFTime {
254                    unit: Unit::Hour,
255                    datetime: ParsedDatetime {
256                        ymd: (1999, 3, 20),
257                        hms: Some((12, 0, 1.0)),
258                        tz: None,
259                        nanosecond: None,
260                    },
261                },
262            ),
263            // Date, time, and timezone
264            (
265                "seconds since 2015-07-04 16:45:30 +02:30",
266                ParsedCFTime {
267                    unit: Unit::Second,
268                    datetime: ParsedDatetime {
269                        ymd: (2015, 7, 4),
270                        hms: Some((16, 45, 30.0)),
271                        tz: Some((2, 30)),
272                        nanosecond: None,
273                    },
274                },
275            ),
276            (
277                "minutes since 2023-12-25 08:00:00 -05:00",
278                ParsedCFTime {
279                    unit: Unit::Minute,
280                    datetime: ParsedDatetime {
281                        ymd: (2023, 12, 25),
282                        hms: Some((8, 0, 0.0)),
283                        tz: Some((-5, 0)),
284                        nanosecond: None,
285                    },
286                },
287            ),
288            (
289                "hour since 2018-09-10 00:00:00 -03:30",
290                ParsedCFTime {
291                    unit: Unit::Hour,
292                    datetime: ParsedDatetime {
293                        ymd: (2018, 9, 10),
294                        hms: Some((0, 0, 0.0)),
295                        tz: Some((-3, 30)),
296                        nanosecond: None,
297                    },
298                },
299            ),
300        ];
301
302        for (input, expected_unit) in units {
303            let result = parse_cf_time(input).unwrap();
304            assert!(result.unit == expected_unit.unit);
305            assert_eq!(result.datetime.ymd, expected_unit.datetime.ymd);
306            assert_eq!(result.datetime.hms, expected_unit.datetime.hms);
307            assert_eq!(result.datetime.tz, expected_unit.datetime.tz);
308            assert_eq!(
309                result.datetime.nanosecond,
310                expected_unit.datetime.nanosecond
311            );
312        }
313    }
314    #[test]
315    fn test_not_valid_date_time_units() {
316        // Test valid date and time units with different combinations
317        let units = vec![
318            "seconds since 2019-06-15 -07:00",
319            "nanoseconds since 2020-01-01 9876543210", // nanoseconds not permitted
320            "invalid_unit since 2023-01-01",           // Invalid unit
321            "hou since 2023-01-01",                    // Missing 'rs' in 'hours'
322            "minutes 2023-01-01",                      // Missing 'since'
323        ];
324
325        for input in units {
326            let result = parse_cf_time(input);
327            assert!(matches!(
328                result.err().unwrap(),
329                crate::errors::Error::UnitParserError(_)
330            ))
331        }
332    }
333    // Add more tests for different valid date and time scenarios
334}