diff --git a/CHANGELOG.md b/CHANGELOG.md index 7035c8c..9a99005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ Versions with only mechanical changes will be omitted from the following list. ### Improvements +* Support a space or `T` in `FromStr` for `DateTime`, meaning that e.g. + `dt.to_string().parse::>()` now correctly works on round-trip. + (@quodlibetor in #378) * Support "negative UTC" in `parse_from_rfc2822` (@quodlibetor #368 reported in #102) diff --git a/src/datetime.rs b/src/datetime.rs index 074b973..8abe304 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -21,7 +21,7 @@ use offset::Local; use offset::{TimeZone, Offset, Utc, FixedOffset}; use naive::{NaiveTime, NaiveDateTime, IsoWeek}; use Date; -use format::{Item, Numeric, Pad, Fixed}; +use format::{Item, Fixed}; use format::{parse, Parsed, ParseError, ParseResult, StrftimeItems}; #[cfg(any(feature = "alloc", feature = "std", test))] use format::DelayedFormat; @@ -628,33 +628,6 @@ impl fmt::Display for DateTime where Tz::Offset: fmt::Display } } -impl str::FromStr for DateTime { - type Err = ParseError; - - fn from_str(s: &str) -> ParseResult> { - const ITEMS: &'static [Item<'static>] = &[ - Item::Numeric(Numeric::Year, Pad::Zero), - Item::Space(""), Item::Literal("-"), - Item::Numeric(Numeric::Month, Pad::Zero), - Item::Space(""), Item::Literal("-"), - Item::Numeric(Numeric::Day, Pad::Zero), - Item::Space(""), Item::Literal("T"), // XXX shouldn't this be case-insensitive? - Item::Numeric(Numeric::Hour, Pad::Zero), - Item::Space(""), Item::Literal(":"), - Item::Numeric(Numeric::Minute, Pad::Zero), - Item::Space(""), Item::Literal(":"), - Item::Numeric(Numeric::Second, Pad::Zero), - Item::Fixed(Fixed::Nanosecond), - Item::Space(""), Item::Fixed(Fixed::TimezoneOffsetZ), - Item::Space(""), - ]; - - let mut parsed = Parsed::new(); - parse(&mut parsed, s, ITEMS.iter())?; - parsed.to_datetime() - } -} - impl str::FromStr for DateTime { type Err = ParseError; @@ -2104,6 +2077,15 @@ mod tests { #[test] fn test_datetime_from_str() { + assert_eq!("2015-02-18T23:16:9.15Z".parse::>(), + Ok(FixedOffset::east(0).ymd(2015, 2, 18).and_hms_milli(23, 16, 9, 150))); + assert_eq!("2015-02-18T23:16:9.15Z".parse::>(), + Ok(Utc.ymd(2015, 2, 18).and_hms_milli(23, 16, 9, 150))); + assert_eq!("2015-02-18T23:16:9.15 UTC".parse::>(), + Ok(Utc.ymd(2015, 2, 18).and_hms_milli(23, 16, 9, 150))); + assert_eq!("2015-02-18T23:16:9.15UTC".parse::>(), + Ok(Utc.ymd(2015, 2, 18).and_hms_milli(23, 16, 9, 150))); + assert_eq!("2015-2-18T23:16:9.15Z".parse::>(), Ok(FixedOffset::east(0).ymd(2015, 2, 18).and_hms_milli(23, 16, 9, 150))); assert_eq!("2015-2-18T13:16:9.15-10:00".parse::>(), @@ -2132,6 +2114,25 @@ mod tests { Ok(Utc.ymd(2013, 8, 9).and_hms(23, 54, 35))); } + #[test] + fn test_to_string_round_trip() { + let dt = Utc.ymd(2000, 1, 1).and_hms(0, 0, 0); + let _dt: DateTime = dt.to_string().parse().unwrap(); + + let ndt_fixed = dt.with_timezone(&FixedOffset::east(3600)); + let _dt: DateTime = ndt_fixed.to_string().parse().unwrap(); + + let ndt_fixed = dt.with_timezone(&FixedOffset::east(0)); + let _dt: DateTime = ndt_fixed.to_string().parse().unwrap(); + } + + #[test] + #[cfg(feature="clock")] + fn test_to_string_round_trip_with_local() { + let ndt = Local::now(); + let _dt: DateTime = ndt.to_string().parse().unwrap(); + } + #[test] #[cfg(feature="clock")] fn test_datetime_format_with_local() { diff --git a/src/format/mod.rs b/src/format/mod.rs index 1099f18..601abe5 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -284,6 +284,7 @@ macro_rules! internal_fix { ($x:ident) => (Item::Fixed(Fixed::Internal(InternalF #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub struct ParseError(ParseErrorKind); +/// The category of parse error #[derive(Debug, Clone, PartialEq, Eq, Copy)] enum ParseErrorKind { /// Given field is out of permitted range. diff --git a/src/format/parse.rs b/src/format/parse.rs index f80f1dc..eb64203 100644 --- a/src/format/parse.rs +++ b/src/format/parse.rs @@ -8,12 +8,13 @@ use core::borrow::Borrow; use core::usize; +use core::str; -use Weekday; - +use {DateTime, FixedOffset, Weekday}; use super::scan; -use super::{Parsed, ParseResult, Item, InternalFixed, InternalInternal}; -use super::{OUT_OF_RANGE, INVALID, TOO_SHORT, TOO_LONG, BAD_FORMAT}; +use super::{Parsed, Numeric, Pad, Fixed, Item, InternalFixed, InternalInternal}; +use super::{ParseResult, ParseError, ParseErrorKind}; +use super::{OUT_OF_RANGE, INVALID, TOO_SHORT, TOO_LONG, BAD_FORMAT, NOT_ENOUGH}; fn set_weekday_with_num_days_from_sunday(p: &mut Parsed, v: i64) -> ParseResult<()> { p.set_weekday(match v { @@ -201,24 +202,39 @@ fn parse_rfc3339<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st /// so one can prepend any number of whitespace then any number of zeroes before numbers. /// /// - (Still) obeying the intrinsic parsing width. This allows, for example, parsing `HHMMSS`. -pub fn parse<'a, I, B>(parsed: &mut Parsed, mut s: &str, items: I) -> ParseResult<()> +pub fn parse<'a, I, B>(parsed: &mut Parsed, s: &str, items: I) -> ParseResult<()> where I: Iterator, B: Borrow> { + parse_internal(parsed, s, items).map(|_| ()).map_err(|(_s, e)| e) +} + +fn parse_internal<'a, 'b, I, B>( + parsed: &mut Parsed, mut s: &'b str, items: I +) -> Result<&'b str, (&'b str, ParseError)> +where I: Iterator, B: Borrow> { macro_rules! try_consume { - ($e:expr) => ({ let (s_, v) = $e?; s = s_; v }) + ($e:expr) => ({ + match $e { + Ok((s_, v)) => { + s = s_; + v + } + Err(e) => return Err((s, e)) + } + }) } for item in items { match item.borrow() { &Item::Literal(prefix) => { - if s.len() < prefix.len() { return Err(TOO_SHORT); } - if !s.starts_with(prefix) { return Err(INVALID); } + if s.len() < prefix.len() { return Err((s, TOO_SHORT)); } + if !s.starts_with(prefix) { return Err((s, INVALID)); } s = &s[prefix.len()..]; } #[cfg(any(feature = "alloc", feature = "std", test))] &Item::OwnedLiteral(ref prefix) => { - if s.len() < prefix.len() { return Err(TOO_SHORT); } - if !s.starts_with(&prefix[..]) { return Err(INVALID); } + if s.len() < prefix.len() { return Err((s, TOO_SHORT)); } + if !s.starts_with(&prefix[..]) { return Err((s, INVALID)); } s = &s[prefix.len()..]; } @@ -265,7 +281,7 @@ pub fn parse<'a, I, B>(parsed: &mut Parsed, mut s: &str, items: I) -> ParseResul let v = if signed { if s.starts_with('-') { let v = try_consume!(scan::number(&s[1..], 1, usize::MAX)); - 0i64.checked_sub(v).ok_or(OUT_OF_RANGE)? + 0i64.checked_sub(v).ok_or((s, OUT_OF_RANGE))? } else if s.starts_with('+') { try_consume!(scan::number(&s[1..], 1, usize::MAX)) } else { @@ -275,7 +291,7 @@ pub fn parse<'a, I, B>(parsed: &mut Parsed, mut s: &str, items: I) -> ParseResul } else { try_consume!(scan::number(s, 1, width)) }; - set(parsed, v)?; + set(parsed, v).map_err(|e| (s, e))?; } &Item::Fixed(ref spec) => { @@ -284,77 +300,77 @@ pub fn parse<'a, I, B>(parsed: &mut Parsed, mut s: &str, items: I) -> ParseResul match spec { &ShortMonthName => { let month0 = try_consume!(scan::short_month0(s)); - parsed.set_month(i64::from(month0) + 1)?; + parsed.set_month(i64::from(month0) + 1).map_err(|e| (s, e))?; } &LongMonthName => { let month0 = try_consume!(scan::short_or_long_month0(s)); - parsed.set_month(i64::from(month0) + 1)?; + parsed.set_month(i64::from(month0) + 1).map_err(|e| (s, e))?; } &ShortWeekdayName => { let weekday = try_consume!(scan::short_weekday(s)); - parsed.set_weekday(weekday)?; + parsed.set_weekday(weekday).map_err(|e| (s, e))?; } &LongWeekdayName => { let weekday = try_consume!(scan::short_or_long_weekday(s)); - parsed.set_weekday(weekday)?; + parsed.set_weekday(weekday).map_err(|e| (s, e))?; } &LowerAmPm | &UpperAmPm => { - if s.len() < 2 { return Err(TOO_SHORT); } + if s.len() < 2 { return Err((s, TOO_SHORT)); } let ampm = match (s.as_bytes()[0] | 32, s.as_bytes()[1] | 32) { (b'a',b'm') => false, (b'p',b'm') => true, - _ => return Err(INVALID) + _ => return Err((s, INVALID)) }; - parsed.set_ampm(ampm)?; + parsed.set_ampm(ampm).map_err(|e| (s, e))?; s = &s[2..]; } &Nanosecond | &Nanosecond3 | &Nanosecond6 | &Nanosecond9 => { if s.starts_with('.') { let nano = try_consume!(scan::nanosecond(&s[1..])); - parsed.set_nanosecond(nano)?; + parsed.set_nanosecond(nano).map_err(|e| (s, e))?; } } &Internal(InternalFixed { val: InternalInternal::Nanosecond3NoDot }) => { - if s.len() < 3 { return Err(TOO_SHORT); } + if s.len() < 3 { return Err((s, TOO_SHORT)); } let nano = try_consume!(scan::nanosecond_fixed(s, 3)); - parsed.set_nanosecond(nano)?; + parsed.set_nanosecond(nano).map_err(|e| (s, e))?; } &Internal(InternalFixed { val: InternalInternal::Nanosecond6NoDot }) => { - if s.len() < 6 { return Err(TOO_SHORT); } + if s.len() < 6 { return Err((s, TOO_SHORT)); } let nano = try_consume!(scan::nanosecond_fixed(s, 6)); - parsed.set_nanosecond(nano)?; + parsed.set_nanosecond(nano).map_err(|e| (s, e))?; } &Internal(InternalFixed { val: InternalInternal::Nanosecond9NoDot }) => { - if s.len() < 9 { return Err(TOO_SHORT); } + if s.len() < 9 { return Err((s, TOO_SHORT)); } let nano = try_consume!(scan::nanosecond_fixed(s, 9)); - parsed.set_nanosecond(nano)?; + parsed.set_nanosecond(nano).map_err(|e| (s, e))?; } - &TimezoneName => return Err(BAD_FORMAT), + &TimezoneName => return Err((s, BAD_FORMAT)), &TimezoneOffsetColon | &TimezoneOffset => { let offset = try_consume!(scan::timezone_offset(s.trim_left(), scan::colon_or_space)); - parsed.set_offset(i64::from(offset))?; + parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?; } &TimezoneOffsetColonZ | &TimezoneOffsetZ => { let offset = try_consume!(scan::timezone_offset_zulu(s.trim_left(), scan::colon_or_space)); - parsed.set_offset(i64::from(offset))?; + parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?; } &Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive }) => { let offset = try_consume!(scan::timezone_offset_permissive( s.trim_left(), scan::colon_or_space)); - parsed.set_offset(i64::from(offset))?; + parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?; } &RFC2822 => try_consume!(parse_rfc2822(parsed, s)), @@ -363,16 +379,54 @@ pub fn parse<'a, I, B>(parsed: &mut Parsed, mut s: &str, items: I) -> ParseResul } &Item::Error => { - return Err(BAD_FORMAT); + return Err((s, BAD_FORMAT)); } } } // if there are trailling chars, it is an error if !s.is_empty() { - Err(TOO_LONG) + Err((s, TOO_LONG)) } else { - Ok(()) + Ok(s) + } +} + +impl str::FromStr for DateTime { + type Err = ParseError; + + fn from_str(s: &str) -> ParseResult> { + const DATE_ITEMS: &'static [Item<'static>] = &[ + Item::Numeric(Numeric::Year, Pad::Zero), + Item::Space(""), Item::Literal("-"), + Item::Numeric(Numeric::Month, Pad::Zero), + Item::Space(""), Item::Literal("-"), + Item::Numeric(Numeric::Day, Pad::Zero), + ]; + const TIME_ITEMS: &'static [Item<'static>] = &[ + Item::Numeric(Numeric::Hour, Pad::Zero), + Item::Space(""), Item::Literal(":"), + Item::Numeric(Numeric::Minute, Pad::Zero), + Item::Space(""), Item::Literal(":"), + Item::Numeric(Numeric::Second, Pad::Zero), + Item::Fixed(Fixed::Nanosecond), + Item::Space(""), Item::Fixed(Fixed::TimezoneOffsetZ), + Item::Space(""), + ]; + + let mut parsed = Parsed::new(); + match parse_internal(&mut parsed, s, DATE_ITEMS.iter()) { + Err((remainder, e)) if e.0 == ParseErrorKind::TooLong =>{ + if remainder.starts_with('T') || remainder.starts_with(' ') { + parse(&mut parsed, &remainder[1..], TIME_ITEMS.iter())?; + } else { + Err(INVALID)?; + } + } + Err((_s, e)) => Err(e)?, + Ok(_) => Err(NOT_ENOUGH)?, + }; + parsed.to_datetime() } } diff --git a/src/format/scan.rs b/src/format/scan.rs index 803c890..fceb3d4 100644 --- a/src/format/scan.rs +++ b/src/format/scan.rs @@ -262,8 +262,20 @@ pub fn timezone_offset_zulu(s: &str, colon: F) -> ParseResult<(&str, i32)> where F: FnMut(&str) -> ParseResult<&str> { - match s.as_bytes().first() { + let bytes = s.as_bytes(); + match bytes.first() { Some(&b'z') | Some(&b'Z') => Ok((&s[1..], 0)), + Some(&b'u') | Some(&b'U') => { + if bytes.len() >= 3 { + let (b, c) = (bytes[1], bytes[2]); + match (b | 32, c | 32) { + (b't', b'c') => Ok((&s[3..], 0)), + _ => Err(INVALID), + } + } else { + Err(INVALID) + } + } _ => timezone_offset(s, colon), } }