diff --git a/src/time/error.rs b/src/time/error.rs index 08c9493..ab9e0ff 100644 --- a/src/time/error.rs +++ b/src/time/error.rs @@ -1,5 +1,5 @@ -use std::{fmt, num::ParseIntError}; use super::parser::Mode; +use std::{fmt, num::ParseIntError}; #[derive(Debug)] pub enum Error { diff --git a/src/time/mod.rs b/src/time/mod.rs index 76cd10c..04ce077 100644 --- a/src/time/mod.rs +++ b/src/time/mod.rs @@ -74,8 +74,8 @@ impl fmt::Display for TimeZone { #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct DateTime { pub year: u32, - pub month: Option, - pub day: Option, + pub month: u8, + pub day: u8, pub hour: Option, pub minute: Option, pub second: Option, @@ -87,11 +87,7 @@ impl fmt::Display for DateTime { write!(f, "{:04}", self.year)?; 'blk: { for field in &[self.month, self.day] { - if let Some(field) = field { - write!(f, "-{field:02}")?; - } else { - break 'blk; - } + write!(f, "-{field:02}")?; } if let Some(h) = self.hour { write!(f, "T{h:02}")?; @@ -122,8 +118,8 @@ mod test { fn fmt_full() { let dt = DateTime { year: 2023, - month: Some(5), - day: Some(7), + month: 5, + day: 7, hour: Some(9), minute: Some(3), second: Some(8), @@ -140,8 +136,8 @@ mod test { fn fmt_partial_offset() { let dt = DateTime { year: 2023, - month: Some(5), - day: Some(7), + month: 5, + day: 7, hour: Some(9), minute: Some(3), second: Some(8), @@ -158,8 +154,8 @@ mod test { fn fmt_utc() { let dt = DateTime { year: 2023, - month: Some(5), - day: Some(7), + month: 5, + day: 7, hour: Some(9), minute: Some(3), second: Some(8), @@ -172,8 +168,8 @@ mod test { fn fmt_no_tz() { let dt = DateTime { year: 2023, - month: Some(5), - day: Some(7), + month: 5, + day: 7, hour: Some(9), minute: Some(3), second: Some(8), @@ -186,8 +182,8 @@ mod test { fn fmt_no_seconds() { let dt = DateTime { year: 2023, - month: Some(5), - day: Some(7), + month: 5, + day: 7, hour: Some(9), minute: Some(3), second: None, @@ -200,8 +196,8 @@ mod test { fn fmt_no_minutes() { let dt = DateTime { year: 2023, - month: Some(5), - day: Some(7), + month: 5, + day: 7, hour: Some(9), minute: None, second: Some(50), @@ -214,8 +210,8 @@ mod test { fn fmt_no_time() { let dt = DateTime { year: 2023, - month: Some(5), - day: Some(7), + month: 5, + day: 7, hour: None, minute: None, second: Some(50), @@ -223,32 +219,4 @@ mod test { }; assert_eq!(dt.to_string(), "2023-05-07Z"); } - - #[test] - fn fmt_no_day() { - let dt = DateTime { - year: 2023, - month: Some(5), - day: None, - hour: Some(20), - minute: None, - second: Some(50), - tz: None, - }; - assert_eq!(dt.to_string(), "2023-05"); - } - - #[test] - fn fmt_no_month() { - let dt = DateTime { - year: 2023, - month: None, - day: None, - hour: Some(20), - minute: None, - second: Some(50), - tz: None, - }; - assert_eq!(dt.to_string(), "2023"); - } } diff --git a/src/time/parser.rs b/src/time/parser.rs index b2aa60f..8d73b8b 100644 --- a/src/time/parser.rs +++ b/src/time/parser.rs @@ -1,5 +1,5 @@ //! Implements a parser for ISO-8601 format Time -use super::{DateTime, Error, Sign, TimeZone, Offset}; +use super::{DateTime, Error, Offset, Sign, TimeZone}; #[derive(Debug, PartialEq)] enum Format { @@ -55,15 +55,15 @@ impl<'a> Parser<'a> { fn validate_day(&self, day: u8) -> Result<(), Error> { let max = match self.month { - Some(1|3|5|7|10|12) => 31, + Some(1 | 3 | 5 | 7 | 10 | 12) => 31, Some(2) => { if self.year.unwrap() % 4 == 0 { 29 } else { 28 } - }, - Some(4|6|9|11) => 30, + } + Some(4 | 6 | 9 | 11) => 30, _ => return Err(Error::InvalidMonth), }; if day > max { @@ -155,14 +155,14 @@ impl<'a> Parser<'a> { 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), } @@ -179,14 +179,14 @@ impl<'a> Parser<'a> { 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() { return Err(Error::UnexpectedChar(self.mode, '-')); @@ -194,14 +194,14 @@ impl<'a> Parser<'a> { self.buffer.push('-'); self.mode = Mode::TimeZone; } - }, + } Mode::TimeZone => { if self.buffer.is_empty() { self.buffer.push('-'); } else { return Err(Error::UnexpectedChar(self.mode, '-')); } - }, + } Mode::Finish => return Err(Error::TrailingGarbage), } Ok(()) @@ -209,21 +209,23 @@ impl<'a> Parser<'a> { fn colon(&mut self) -> Result<(), Error> { match self.mode { - Mode::Year | Mode::Month | Mode::Day => return Err(Error::UnexpectedChar(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, ':')); } else { self.sep = true; } - }, + } Mode::TimeZone => { if !self.buffer.len() == 2 || self.format == Format::Basic { return Err(Error::UnexpectedChar(self.mode, ':')); } else { self.sep = true; } - }, + } Mode::Finish => return Err(Error::TrailingGarbage), } Ok(()) @@ -239,7 +241,11 @@ impl<'a> Parser<'a> { } fn zed(&mut self) -> Result<(), Error> { - if self.mode == Mode::Year || !self.buffer.is_empty() { + if self.mode == Mode::Year + || self.mode == Mode::Month + || self.mode == Mode::Day + || !self.buffer.is_empty() + { return Err(Error::UnexpectedChar(self.mode, 'Z')); } else { self.tz = Some(TimeZone::UTC); @@ -248,6 +254,17 @@ impl<'a> Parser<'a> { Ok(()) } + fn plus(&mut self) -> Result<(), Error> { + if self.mode != Mode::TimeZone && self.mode != Mode::Finish || !self.buffer.is_empty() { + return Err(Error::UnexpectedChar(self.mode, '+')); + } else if self.mode == Mode::Year || self.mode == Mode::Month || self.mode == Mode::Day { + return Err(Error::UnexpectedChar(self.mode, '+')); + } else { + self.buffer.push('+'); + } + Ok(()) + } + fn parse_tz_basic(&mut self) -> Result<(), Error> { let sign = match self.buffer.chars().next() { Some('+') => Sign::Positive, @@ -257,8 +274,9 @@ impl<'a> Parser<'a> { let Some(n) = self.buffer.get(1..3) else { return Err(Error::InvalidOffset); }; + println!("Hours: {n}"); let hours: u8 = n.parse()?; - let minutes: Option = if let Some(n) = self.buffer.get(3..) { + let minutes: Option = if let Some(n) = self.buffer.get(3..5) { Some(n.parse()?) } else { None @@ -266,13 +284,19 @@ impl<'a> Parser<'a> { 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 })), + _ => { + self.tz = Some(TimeZone::Offset(Offset { + sign, + hours, + minutes, + })) + } } Ok(()) } fn parse_tz_extended(&mut self) -> Result<(), Error> { - if !self.sep { + if self.buffer.len() > 3 && !self.sep { Err(Error::MissingSeparator) } else { self.parse_tz_basic() @@ -287,6 +311,7 @@ impl<'a> Parser<'a> { ':' => self.colon()?, 'T' => self.tee()?, 'Z' => self.zed()?, + '+' => self.plus()?, _ => return Err(Error::UnexpectedChar(self.mode, c)), } } @@ -301,13 +326,16 @@ impl<'a> Parser<'a> { Format::Extended => self.parse_tz_extended()?, } } - }, + } _ => return Err(Error::Truncated), } + if self.year.is_none() || self.month.is_none() || self.day.is_none() { + return Err(Error::Truncated); + } Ok(DateTime { year: self.year.unwrap(), - month: self.month, - day: self.day, + month: self.month.unwrap(), + day: self.day.unwrap(), hour: self.hour, minute: self.minute, second: self.second, @@ -325,12 +353,91 @@ mod tests { let parser = Parser::new("2023-05-09T19:39:15Z"); let dt = parser.parse().unwrap(); assert_eq!(dt.year, 2023); - assert_eq!(dt.month, Some(5)); - assert_eq!(dt.day, Some(9)); + 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_full_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_full_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_full_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_full_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_full_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()); + } +}