//! Implements a parser for ISO-8601 format Time use super::{days_in_month, DateTime, Error, Offset, Sign, TimeZone}; #[derive(Debug, PartialEq)] enum Format { Basic, Extended, } #[derive(Clone, Copy, Debug, PartialEq)] pub enum Mode { Year, Month, Day, Hour, Minute, Second, TimeZone, Finish, } #[derive(Debug)] pub struct Parser<'a> { text: &'a str, format: Format, mode: Mode, buffer: String, year: Option, month: Option, day: Option, sep: bool, hour: Option, minute: Option, second: Option, tz: Option, } impl<'a> Parser<'a> { pub fn new(text: &'a str) -> Self { Self { text, format: Format::Basic, buffer: String::new(), mode: Mode::Year, year: None, month: None, day: None, sep: false, hour: None, minute: None, second: None, tz: None, } } #[allow(clippy::missing_panics_doc)] fn validate_day(&self, day: u8) -> Result<(), Error> { let max = days_in_month(self.month.unwrap(), self.year.unwrap()); if day > max { Err(Error::InvalidDay) } else { Ok(()) } } fn number(&mut self, c: char) -> Result<(), Error> { self.buffer.push(c); match self.mode { Mode::Year => { if self.buffer.len() == 4 { let y: u32 = self.buffer.parse()?; self.buffer.clear(); self.year = Some(y); self.mode = Mode::Month; } } Mode::Month => { if self.buffer.len() == 2 { let m: u8 = self.buffer.parse()?; if m > 12 { return Err(Error::InvalidMonth); } self.buffer.clear(); self.month = Some(m); self.mode = Mode::Day; self.sep = false; } } Mode::Day => { if self.buffer.len() == 2 { let d: u8 = self.buffer.parse()?; self.validate_day(d)?; self.buffer.clear(); self.day = Some(d); self.mode = Mode::Hour; self.sep = false; } else if self.format == Format::Extended && !self.sep { return Err(Error::MissingSeparator); } } Mode::Hour => { if self.buffer.len() == 2 { let h: u8 = self.buffer.parse()?; if h > 23 { return Err(Error::InvalidHour); } self.buffer.clear(); self.hour = Some(h); self.mode = Mode::Minute; self.sep = false; } else if self.format == Format::Extended && !self.sep { return Err(Error::MissingSeparator); } } Mode::Minute => { if self.buffer.len() == 2 { let m = self.buffer.parse()?; if m > 59 { return Err(Error::InvalidMinute); } self.buffer.clear(); self.minute = Some(m); self.mode = Mode::Second; self.sep = false; } else if self.format == Format::Extended && !self.sep { return Err(Error::MissingSeparator); } } Mode::Second => { if self.buffer.len() == 2 { let s = self.buffer.parse()?; if s > 59 { return Err(Error::InvalidSecond); } self.buffer.clear(); self.second = Some(s); self.mode = Mode::TimeZone; self.sep = false; } else if self.format == Format::Extended && !self.sep { return Err(Error::MissingSeparator); } } Mode::TimeZone => match self.format { Format::Basic => { if self.buffer.len() == 5 { self.mode = Mode::Finish; } } Format::Extended => { if self.buffer.len() > 3 && !self.sep { return Err(Error::MissingSeparator); } else if self.buffer.len() == 5 { self.mode = Mode::Finish; } } }, Mode::Finish => return Err(Error::TrailingGarbage), } Ok(()) } fn dash(&mut self) -> Result<(), Error> { match self.mode { Mode::Year => return Err(Error::UnexpectedChar(Mode::Year, '-')), Mode::Month => { if self.month.is_some() { return Err(Error::UnexpectedChar(self.mode, '-')); } self.format = Format::Extended; self.sep = true; } Mode::Day => { if self.day.is_some() || self.format == Format::Basic { return Err(Error::UnexpectedChar(self.mode, '-')); } else { self.sep = true; } } Mode::Hour | Mode::Minute | Mode::Second => { if self.buffer.is_empty() { self.buffer.push('-'); self.mode = Mode::TimeZone; } else { return Err(Error::UnexpectedChar(self.mode, '-')); } } Mode::TimeZone => { if self.buffer.is_empty() { self.buffer.push('-'); } else { return Err(Error::UnexpectedChar(self.mode, '-')); } } Mode::Finish => return Err(Error::TrailingGarbage), } Ok(()) } fn colon(&mut self) -> Result<(), Error> { match self.mode { Mode::Year | Mode::Month | Mode::Day => { return Err(Error::UnexpectedChar(self.mode, ':')) } Mode::Hour | Mode::Minute | Mode::Second => { if !self.buffer.is_empty() || self.format == Format::Basic { return Err(Error::UnexpectedChar(self.mode, ':')); } self.sep = true; } Mode::TimeZone => { if !self.buffer.len() == 2 || self.format == Format::Basic { return Err(Error::UnexpectedChar(self.mode, ':')); } self.sep = true; } Mode::Finish => return Err(Error::TrailingGarbage), } Ok(()) } fn tee(&mut self) -> Result<(), Error> { if self.mode != Mode::Hour || !self.buffer.is_empty() { return Err(Error::UnexpectedChar(self.mode, 'T')); } self.sep = true; Ok(()) } fn zed(&mut self) -> Result<(), Error> { if self.mode == Mode::Year || self.mode == Mode::Month || self.mode == Mode::Day || !self.buffer.is_empty() { return Err(Error::UnexpectedChar(self.mode, 'Z')); } self.tz = Some(TimeZone::UTC); self.mode = Mode::Finish; Ok(()) } fn plus(&mut self) -> Result<(), Error> { if self.mode != Mode::TimeZone && self.mode != Mode::Finish || !self.buffer.is_empty() || self.mode == Mode::Year || self.mode == Mode::Month || self.mode == Mode::Day { return Err(Error::UnexpectedChar(self.mode, '+')); } self.buffer.push('+'); Ok(()) } fn parse_tz_basic(&mut self) -> Result<(), Error> { let sign = match self.buffer.chars().next() { Some('+') => Sign::Positive, Some('-') => Sign::Negative, None => return Ok(()), _ => return Err(Error::InvalidOffset), }; let Some(n) = self.buffer.get(1..3) else { return Err(Error::InvalidOffset); }; let hours: u8 = n.parse()?; let minutes: Option = if let Some(n) = self.buffer.get(3..5) { Some(n.parse()?) } else { None }; match (hours, minutes) { (h, None) if h > 12 => return Err(Error::InvalidOffset), (h, Some(m)) if h == 12 && m > 0 || m > 59 => return Err(Error::InvalidOffset), _ => { self.tz = Some(TimeZone::Offset(Offset { sign, hours, minutes, })); } } Ok(()) } fn parse_tz_extended(&mut self) -> Result<(), Error> { if self.buffer.len() > 3 && !self.sep { Err(Error::MissingSeparator) } else { self.parse_tz_basic() } } pub fn parse(mut self) -> Result { for c in self.text.chars() { match c { x if x.is_numeric() => self.number(c)?, '-' => self.dash()?, ':' => self.colon()?, 'T' => self.tee()?, 'Z' => self.zed()?, '+' => self.plus()?, _ => return Err(Error::UnexpectedChar(self.mode, c)), } } match self.mode { Mode::TimeZone | Mode::Finish => { if self.tz == Some(TimeZone::UTC) && !self.buffer.is_empty() { return Err(Error::TrailingGarbage); } if self.tz.is_none() { match self.format { Format::Basic => self.parse_tz_basic()?, Format::Extended => self.parse_tz_extended()?, } } } _ => {} } if let Some(year) = self.year { if let Some(month) = self.month { if let Some(day) = self.day { return Ok(DateTime { year, month, day, hour: self.hour, minute: self.minute, second: self.second, tz: self.tz, }); } } } Err(Error::Truncated) } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_extended_utc() { let parser = Parser::new("2023-05-09T19:39:15Z"); let dt = parser.parse().unwrap(); assert_eq!(dt.year, 2023); assert_eq!(dt.month, 5); assert_eq!(dt.day, 9); assert_eq!(dt.hour, Some(19)); assert_eq!(dt.minute, Some(39)); assert_eq!(dt.second, Some(15)); assert_eq!(dt.tz, Some(TimeZone::UTC)); } #[test] fn parse_extended_positive() { let parser = Parser::new("2023-05-09T19:39:15+05:15"); let dt = parser.parse().unwrap(); assert_eq!(dt.year, 2023); assert_eq!(dt.month, 5); assert_eq!(dt.day, 9); assert_eq!(dt.hour, Some(19)); assert_eq!(dt.minute, Some(39)); assert_eq!(dt.second, Some(15)); assert_eq!( dt.tz, Some(TimeZone::Offset(Offset { sign: Sign::Positive, hours: 5, minutes: Some(15) })) ); } #[test] fn parse_extended_negative() { let parser = Parser::new("2023-05-09T19:39:15-05"); let dt = parser.parse().unwrap(); assert_eq!(dt.year, 2023); assert_eq!(dt.month, 5); assert_eq!(dt.day, 9); assert_eq!(dt.hour, Some(19)); assert_eq!(dt.minute, Some(39)); assert_eq!(dt.second, Some(15)); assert_eq!( dt.tz, Some(TimeZone::Offset(Offset { sign: Sign::Negative, hours: 5, minutes: None })) ); } #[test] fn parse_extended_no_seconds() { let parser = Parser::new("2023-05-09T19:39-05"); let dt = parser.parse().unwrap(); assert!(dt.second.is_none()); assert_eq!( dt.tz, Some(TimeZone::Offset(Offset { sign: Sign::Negative, hours: 5, minutes: None })) ); } #[test] fn parse_extended_no_minutes() { let parser = Parser::new("2023-05-09T19-05"); let dt = parser.parse().unwrap(); assert!(dt.second.is_none()); assert!(dt.minute.is_none()); assert_eq!( dt.tz, Some(TimeZone::Offset(Offset { sign: Sign::Negative, hours: 5, minutes: None })) ); } #[test] fn parse_extended_no_time() { let parser = Parser::new("2023-05-09"); let dt = parser.parse().unwrap(); assert!(dt.second.is_none()); assert!(dt.minute.is_none()); assert!(dt.tz.is_none()); } #[test] fn parse_basic_utc() { let parser = Parser::new("20230407T114230Z"); let dt = parser.parse().unwrap(); assert_eq!(dt.year, 2023); assert_eq!(dt.month, 4); assert_eq!(dt.day, 7); assert_eq!(dt.hour, Some(11)); assert_eq!(dt.minute, Some(42)); assert_eq!(dt.second, Some(30)); assert_eq!(dt.tz, Some(TimeZone::UTC)); } #[test] fn parse_basic_positive() { let parser = Parser::new("20230407T114230+0520"); let dt = parser.parse().unwrap(); assert_eq!( dt.tz, Some(TimeZone::Offset(Offset { sign: Sign::Positive, hours: 5, minutes: Some(20) })) ); } #[test] fn parse_basic_negative() { let parser = Parser::new("20230407T114230-0520"); let dt = parser.parse().unwrap(); assert_eq!( dt.tz, Some(TimeZone::Offset(Offset { sign: Sign::Negative, hours: 5, minutes: Some(20) })) ); } #[test] fn parse_basic_no_seconds() { let parser = Parser::new("20230407T1142-0520"); let dt = parser.parse().unwrap(); assert!(dt.second.is_none()); assert_eq!( dt.tz, Some(TimeZone::Offset(Offset { sign: Sign::Negative, hours: 5, minutes: Some(20) })) ); } #[test] fn parse_basic_no_minutes() { let parser = Parser::new("20230407T11-05"); let dt = parser.parse().unwrap(); assert!(dt.second.is_none()); assert!(dt.minute.is_none()); assert_eq!( dt.tz, Some(TimeZone::Offset(Offset { sign: Sign::Negative, hours: 5, minutes: None })) ); } #[test] fn parse_no_tz() { let dt: Result = Parser::new("2023-06-11T22:41:31").parse(); assert!(dt.unwrap().tz.is_none()); } }