Added some tests for time::Parser

This commit is contained in:
Nathan Fisher 2023-06-10 00:08:33 -04:00
parent 0bbd11aa03
commit de27339b73
3 changed files with 148 additions and 73 deletions

View file

@ -1,5 +1,5 @@
use std::{fmt, num::ParseIntError};
use super::parser::Mode; use super::parser::Mode;
use std::{fmt, num::ParseIntError};
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {

View file

@ -74,8 +74,8 @@ impl fmt::Display for TimeZone {
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct DateTime { pub struct DateTime {
pub year: u32, pub year: u32,
pub month: Option<u8>, pub month: u8,
pub day: Option<u8>, pub day: u8,
pub hour: Option<u8>, pub hour: Option<u8>,
pub minute: Option<u8>, pub minute: Option<u8>,
pub second: Option<u8>, pub second: Option<u8>,
@ -87,11 +87,7 @@ impl fmt::Display for DateTime {
write!(f, "{:04}", self.year)?; write!(f, "{:04}", self.year)?;
'blk: { 'blk: {
for field in &[self.month, self.day] { for field in &[self.month, self.day] {
if let Some(field) = field { write!(f, "-{field:02}")?;
write!(f, "-{field:02}")?;
} else {
break 'blk;
}
} }
if let Some(h) = self.hour { if let Some(h) = self.hour {
write!(f, "T{h:02}")?; write!(f, "T{h:02}")?;
@ -122,8 +118,8 @@ mod test {
fn fmt_full() { fn fmt_full() {
let dt = DateTime { let dt = DateTime {
year: 2023, year: 2023,
month: Some(5), month: 5,
day: Some(7), day: 7,
hour: Some(9), hour: Some(9),
minute: Some(3), minute: Some(3),
second: Some(8), second: Some(8),
@ -140,8 +136,8 @@ mod test {
fn fmt_partial_offset() { fn fmt_partial_offset() {
let dt = DateTime { let dt = DateTime {
year: 2023, year: 2023,
month: Some(5), month: 5,
day: Some(7), day: 7,
hour: Some(9), hour: Some(9),
minute: Some(3), minute: Some(3),
second: Some(8), second: Some(8),
@ -158,8 +154,8 @@ mod test {
fn fmt_utc() { fn fmt_utc() {
let dt = DateTime { let dt = DateTime {
year: 2023, year: 2023,
month: Some(5), month: 5,
day: Some(7), day: 7,
hour: Some(9), hour: Some(9),
minute: Some(3), minute: Some(3),
second: Some(8), second: Some(8),
@ -172,8 +168,8 @@ mod test {
fn fmt_no_tz() { fn fmt_no_tz() {
let dt = DateTime { let dt = DateTime {
year: 2023, year: 2023,
month: Some(5), month: 5,
day: Some(7), day: 7,
hour: Some(9), hour: Some(9),
minute: Some(3), minute: Some(3),
second: Some(8), second: Some(8),
@ -186,8 +182,8 @@ mod test {
fn fmt_no_seconds() { fn fmt_no_seconds() {
let dt = DateTime { let dt = DateTime {
year: 2023, year: 2023,
month: Some(5), month: 5,
day: Some(7), day: 7,
hour: Some(9), hour: Some(9),
minute: Some(3), minute: Some(3),
second: None, second: None,
@ -200,8 +196,8 @@ mod test {
fn fmt_no_minutes() { fn fmt_no_minutes() {
let dt = DateTime { let dt = DateTime {
year: 2023, year: 2023,
month: Some(5), month: 5,
day: Some(7), day: 7,
hour: Some(9), hour: Some(9),
minute: None, minute: None,
second: Some(50), second: Some(50),
@ -214,8 +210,8 @@ mod test {
fn fmt_no_time() { fn fmt_no_time() {
let dt = DateTime { let dt = DateTime {
year: 2023, year: 2023,
month: Some(5), month: 5,
day: Some(7), day: 7,
hour: None, hour: None,
minute: None, minute: None,
second: Some(50), second: Some(50),
@ -223,32 +219,4 @@ mod test {
}; };
assert_eq!(dt.to_string(), "2023-05-07Z"); 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");
}
} }

View file

@ -1,5 +1,5 @@
//! Implements a parser for ISO-8601 format Time //! 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)] #[derive(Debug, PartialEq)]
enum Format { enum Format {
@ -55,15 +55,15 @@ impl<'a> Parser<'a> {
fn validate_day(&self, day: u8) -> Result<(), Error> { fn validate_day(&self, day: u8) -> Result<(), Error> {
let max = match self.month { let max = match self.month {
Some(1|3|5|7|10|12) => 31, Some(1 | 3 | 5 | 7 | 10 | 12) => 31,
Some(2) => { Some(2) => {
if self.year.unwrap() % 4 == 0 { if self.year.unwrap() % 4 == 0 {
29 29
} else { } else {
28 28
} }
}, }
Some(4|6|9|11) => 30, Some(4 | 6 | 9 | 11) => 30,
_ => return Err(Error::InvalidMonth), _ => return Err(Error::InvalidMonth),
}; };
if day > max { if day > max {
@ -155,14 +155,14 @@ impl<'a> Parser<'a> {
if self.buffer.len() == 5 { if self.buffer.len() == 5 {
self.mode = Mode::Finish; self.mode = Mode::Finish;
} }
}, }
Format::Extended => { Format::Extended => {
if self.buffer.len() > 3 && !self.sep { if self.buffer.len() > 3 && !self.sep {
return Err(Error::MissingSeparator); return Err(Error::MissingSeparator);
} else if self.buffer.len() == 5 { } else if self.buffer.len() == 5 {
self.mode = Mode::Finish; self.mode = Mode::Finish;
} }
}, }
}, },
Mode::Finish => return Err(Error::TrailingGarbage), Mode::Finish => return Err(Error::TrailingGarbage),
} }
@ -179,14 +179,14 @@ impl<'a> Parser<'a> {
self.format = Format::Extended; self.format = Format::Extended;
self.sep = true; self.sep = true;
} }
}, }
Mode::Day => { Mode::Day => {
if self.day.is_some() || self.format == Format::Basic { if self.day.is_some() || self.format == Format::Basic {
return Err(Error::UnexpectedChar(self.mode, '-')); return Err(Error::UnexpectedChar(self.mode, '-'));
} else { } else {
self.sep = true; self.sep = true;
} }
}, }
Mode::Hour | Mode::Minute | Mode::Second => { Mode::Hour | Mode::Minute | Mode::Second => {
if !self.buffer.is_empty() { if !self.buffer.is_empty() {
return Err(Error::UnexpectedChar(self.mode, '-')); return Err(Error::UnexpectedChar(self.mode, '-'));
@ -194,14 +194,14 @@ impl<'a> Parser<'a> {
self.buffer.push('-'); self.buffer.push('-');
self.mode = Mode::TimeZone; self.mode = Mode::TimeZone;
} }
}, }
Mode::TimeZone => { Mode::TimeZone => {
if self.buffer.is_empty() { if self.buffer.is_empty() {
self.buffer.push('-'); self.buffer.push('-');
} else { } else {
return Err(Error::UnexpectedChar(self.mode, '-')); return Err(Error::UnexpectedChar(self.mode, '-'));
} }
}, }
Mode::Finish => return Err(Error::TrailingGarbage), Mode::Finish => return Err(Error::TrailingGarbage),
} }
Ok(()) Ok(())
@ -209,21 +209,23 @@ impl<'a> Parser<'a> {
fn colon(&mut self) -> Result<(), Error> { fn colon(&mut self) -> Result<(), Error> {
match self.mode { 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 => { Mode::Hour | Mode::Minute | Mode::Second => {
if !self.buffer.is_empty() || self.format == Format::Basic { if !self.buffer.is_empty() || self.format == Format::Basic {
return Err(Error::UnexpectedChar(self.mode, ':')); return Err(Error::UnexpectedChar(self.mode, ':'));
} else { } else {
self.sep = true; self.sep = true;
} }
}, }
Mode::TimeZone => { Mode::TimeZone => {
if !self.buffer.len() == 2 || self.format == Format::Basic { if !self.buffer.len() == 2 || self.format == Format::Basic {
return Err(Error::UnexpectedChar(self.mode, ':')); return Err(Error::UnexpectedChar(self.mode, ':'));
} else { } else {
self.sep = true; self.sep = true;
} }
}, }
Mode::Finish => return Err(Error::TrailingGarbage), Mode::Finish => return Err(Error::TrailingGarbage),
} }
Ok(()) Ok(())
@ -239,7 +241,11 @@ impl<'a> Parser<'a> {
} }
fn zed(&mut self) -> Result<(), Error> { 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')); return Err(Error::UnexpectedChar(self.mode, 'Z'));
} else { } else {
self.tz = Some(TimeZone::UTC); self.tz = Some(TimeZone::UTC);
@ -248,6 +254,17 @@ impl<'a> Parser<'a> {
Ok(()) 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> { fn parse_tz_basic(&mut self) -> Result<(), Error> {
let sign = match self.buffer.chars().next() { let sign = match self.buffer.chars().next() {
Some('+') => Sign::Positive, Some('+') => Sign::Positive,
@ -257,8 +274,9 @@ impl<'a> Parser<'a> {
let Some(n) = self.buffer.get(1..3) else { let Some(n) = self.buffer.get(1..3) else {
return Err(Error::InvalidOffset); return Err(Error::InvalidOffset);
}; };
println!("Hours: {n}");
let hours: u8 = n.parse()?; let hours: u8 = n.parse()?;
let minutes: Option<u8> = if let Some(n) = self.buffer.get(3..) { let minutes: Option<u8> = if let Some(n) = self.buffer.get(3..5) {
Some(n.parse()?) Some(n.parse()?)
} else { } else {
None None
@ -266,13 +284,19 @@ impl<'a> Parser<'a> {
match (hours, minutes) { match (hours, minutes) {
(h, None) if h > 12 => return Err(Error::InvalidOffset), (h, None) if h > 12 => return Err(Error::InvalidOffset),
(h, Some(m)) if h == 12 && m > 0 || m > 59 => 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(()) Ok(())
} }
fn parse_tz_extended(&mut self) -> Result<(), Error> { fn parse_tz_extended(&mut self) -> Result<(), Error> {
if !self.sep { if self.buffer.len() > 3 && !self.sep {
Err(Error::MissingSeparator) Err(Error::MissingSeparator)
} else { } else {
self.parse_tz_basic() self.parse_tz_basic()
@ -287,6 +311,7 @@ impl<'a> Parser<'a> {
':' => self.colon()?, ':' => self.colon()?,
'T' => self.tee()?, 'T' => self.tee()?,
'Z' => self.zed()?, 'Z' => self.zed()?,
'+' => self.plus()?,
_ => return Err(Error::UnexpectedChar(self.mode, c)), _ => return Err(Error::UnexpectedChar(self.mode, c)),
} }
} }
@ -301,13 +326,16 @@ impl<'a> Parser<'a> {
Format::Extended => self.parse_tz_extended()?, Format::Extended => self.parse_tz_extended()?,
} }
} }
}, }
_ => return Err(Error::Truncated), _ => return Err(Error::Truncated),
} }
if self.year.is_none() || self.month.is_none() || self.day.is_none() {
return Err(Error::Truncated);
}
Ok(DateTime { Ok(DateTime {
year: self.year.unwrap(), year: self.year.unwrap(),
month: self.month, month: self.month.unwrap(),
day: self.day, day: self.day.unwrap(),
hour: self.hour, hour: self.hour,
minute: self.minute, minute: self.minute,
second: self.second, second: self.second,
@ -325,12 +353,91 @@ mod tests {
let parser = Parser::new("2023-05-09T19:39:15Z"); let parser = Parser::new("2023-05-09T19:39:15Z");
let dt = parser.parse().unwrap(); let dt = parser.parse().unwrap();
assert_eq!(dt.year, 2023); assert_eq!(dt.year, 2023);
assert_eq!(dt.month, Some(5)); assert_eq!(dt.month, 5);
assert_eq!(dt.day, Some(9)); assert_eq!(dt.day, 9);
assert_eq!(dt.hour, Some(19)); assert_eq!(dt.hour, Some(19));
assert_eq!(dt.minute, Some(39)); assert_eq!(dt.minute, Some(39));
assert_eq!(dt.second, Some(15)); assert_eq!(dt.second, Some(15));
assert_eq!(dt.tz, Some(TimeZone::UTC)); 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());
}
}