support round tripping display <-> datetime

This extends `FromStr` to allow either a `T` or a ` ` (space) as the delimiter
between the date and the time, and, because of the fact that the `Z`
parser-specifier is shared with the Fixed notation, extends the fixed notation
to support `UTC` in addition to `Z` as the zero-offset.

IMO this Fixes #147
This commit is contained in:
Brandon W Maister 2019-12-26 23:41:36 -05:00
parent b9cd0ce803
commit b9c967b2ac
5 changed files with 133 additions and 62 deletions

View File

@ -12,6 +12,9 @@ Versions with only mechanical changes will be omitted from the following list.
### Improvements ### Improvements
* Support a space or `T` in `FromStr` for `DateTime<Tz>`, meaning that e.g.
`dt.to_string().parse::<DateTime<Utc>>()` now correctly works on round-trip.
(@quodlibetor in #378)
* Support "negative UTC" in `parse_from_rfc2822` (@quodlibetor #368 reported in * Support "negative UTC" in `parse_from_rfc2822` (@quodlibetor #368 reported in
#102) #102)

View File

@ -21,7 +21,7 @@ use offset::Local;
use offset::{TimeZone, Offset, Utc, FixedOffset}; use offset::{TimeZone, Offset, Utc, FixedOffset};
use naive::{NaiveTime, NaiveDateTime, IsoWeek}; use naive::{NaiveTime, NaiveDateTime, IsoWeek};
use Date; use Date;
use format::{Item, Numeric, Pad, Fixed}; use format::{Item, Fixed};
use format::{parse, Parsed, ParseError, ParseResult, StrftimeItems}; use format::{parse, Parsed, ParseError, ParseResult, StrftimeItems};
#[cfg(any(feature = "alloc", feature = "std", test))] #[cfg(any(feature = "alloc", feature = "std", test))]
use format::DelayedFormat; use format::DelayedFormat;
@ -628,33 +628,6 @@ impl<Tz: TimeZone> fmt::Display for DateTime<Tz> where Tz::Offset: fmt::Display
} }
} }
impl str::FromStr for DateTime<FixedOffset> {
type Err = ParseError;
fn from_str(s: &str) -> ParseResult<DateTime<FixedOffset>> {
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<Utc> { impl str::FromStr for DateTime<Utc> {
type Err = ParseError; type Err = ParseError;
@ -2104,6 +2077,15 @@ mod tests {
#[test] #[test]
fn test_datetime_from_str() { fn test_datetime_from_str() {
assert_eq!("2015-02-18T23:16:9.15Z".parse::<DateTime<FixedOffset>>(),
Ok(FixedOffset::east(0).ymd(2015, 2, 18).and_hms_milli(23, 16, 9, 150)));
assert_eq!("2015-02-18T23:16:9.15Z".parse::<DateTime<Utc>>(),
Ok(Utc.ymd(2015, 2, 18).and_hms_milli(23, 16, 9, 150)));
assert_eq!("2015-02-18T23:16:9.15 UTC".parse::<DateTime<Utc>>(),
Ok(Utc.ymd(2015, 2, 18).and_hms_milli(23, 16, 9, 150)));
assert_eq!("2015-02-18T23:16:9.15UTC".parse::<DateTime<Utc>>(),
Ok(Utc.ymd(2015, 2, 18).and_hms_milli(23, 16, 9, 150)));
assert_eq!("2015-2-18T23:16:9.15Z".parse::<DateTime<FixedOffset>>(), assert_eq!("2015-2-18T23:16:9.15Z".parse::<DateTime<FixedOffset>>(),
Ok(FixedOffset::east(0).ymd(2015, 2, 18).and_hms_milli(23, 16, 9, 150))); 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::<DateTime<FixedOffset>>(), assert_eq!("2015-2-18T13:16:9.15-10:00".parse::<DateTime<FixedOffset>>(),
@ -2132,6 +2114,25 @@ mod tests {
Ok(Utc.ymd(2013, 8, 9).and_hms(23, 54, 35))); 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<Utc> = dt.to_string().parse().unwrap();
let ndt_fixed = dt.with_timezone(&FixedOffset::east(3600));
let _dt: DateTime<FixedOffset> = ndt_fixed.to_string().parse().unwrap();
let ndt_fixed = dt.with_timezone(&FixedOffset::east(0));
let _dt: DateTime<FixedOffset> = 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<FixedOffset> = ndt.to_string().parse().unwrap();
}
#[test] #[test]
#[cfg(feature="clock")] #[cfg(feature="clock")]
fn test_datetime_format_with_local() { fn test_datetime_format_with_local() {

View File

@ -284,6 +284,7 @@ macro_rules! internal_fix { ($x:ident) => (Item::Fixed(Fixed::Internal(InternalF
#[derive(Debug, Clone, PartialEq, Eq, Copy)] #[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub struct ParseError(ParseErrorKind); pub struct ParseError(ParseErrorKind);
/// The category of parse error
#[derive(Debug, Clone, PartialEq, Eq, Copy)] #[derive(Debug, Clone, PartialEq, Eq, Copy)]
enum ParseErrorKind { enum ParseErrorKind {
/// Given field is out of permitted range. /// Given field is out of permitted range.

View File

@ -8,12 +8,13 @@
use core::borrow::Borrow; use core::borrow::Borrow;
use core::usize; use core::usize;
use core::str;
use Weekday; use {DateTime, FixedOffset, Weekday};
use super::scan; use super::scan;
use super::{Parsed, ParseResult, Item, InternalFixed, InternalInternal}; use super::{Parsed, Numeric, Pad, Fixed, Item, InternalFixed, InternalInternal};
use super::{OUT_OF_RANGE, INVALID, TOO_SHORT, TOO_LONG, BAD_FORMAT}; 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<()> { fn set_weekday_with_num_days_from_sunday(p: &mut Parsed, v: i64) -> ParseResult<()> {
p.set_weekday(match v { 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. /// 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`. /// - (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<Item=B>, B: Borrow<Item<'a>> { where I: Iterator<Item=B>, B: Borrow<Item<'a>> {
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<Item=B>, B: Borrow<Item<'a>> {
macro_rules! try_consume { 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 { for item in items {
match item.borrow() { match item.borrow() {
&Item::Literal(prefix) => { &Item::Literal(prefix) => {
if s.len() < prefix.len() { return Err(TOO_SHORT); } if s.len() < prefix.len() { return Err((s, TOO_SHORT)); }
if !s.starts_with(prefix) { return Err(INVALID); } if !s.starts_with(prefix) { return Err((s, INVALID)); }
s = &s[prefix.len()..]; s = &s[prefix.len()..];
} }
#[cfg(any(feature = "alloc", feature = "std", test))] #[cfg(any(feature = "alloc", feature = "std", test))]
&Item::OwnedLiteral(ref prefix) => { &Item::OwnedLiteral(ref prefix) => {
if s.len() < prefix.len() { return Err(TOO_SHORT); } if s.len() < prefix.len() { return Err((s, TOO_SHORT)); }
if !s.starts_with(&prefix[..]) { return Err(INVALID); } if !s.starts_with(&prefix[..]) { return Err((s, INVALID)); }
s = &s[prefix.len()..]; 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 { let v = if signed {
if s.starts_with('-') { if s.starts_with('-') {
let v = try_consume!(scan::number(&s[1..], 1, usize::MAX)); 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('+') { } else if s.starts_with('+') {
try_consume!(scan::number(&s[1..], 1, usize::MAX)) try_consume!(scan::number(&s[1..], 1, usize::MAX))
} else { } else {
@ -275,7 +291,7 @@ pub fn parse<'a, I, B>(parsed: &mut Parsed, mut s: &str, items: I) -> ParseResul
} else { } else {
try_consume!(scan::number(s, 1, width)) try_consume!(scan::number(s, 1, width))
}; };
set(parsed, v)?; set(parsed, v).map_err(|e| (s, e))?;
} }
&Item::Fixed(ref spec) => { &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 { match spec {
&ShortMonthName => { &ShortMonthName => {
let month0 = try_consume!(scan::short_month0(s)); 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 => { &LongMonthName => {
let month0 = try_consume!(scan::short_or_long_month0(s)); 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 => { &ShortWeekdayName => {
let weekday = try_consume!(scan::short_weekday(s)); let weekday = try_consume!(scan::short_weekday(s));
parsed.set_weekday(weekday)?; parsed.set_weekday(weekday).map_err(|e| (s, e))?;
} }
&LongWeekdayName => { &LongWeekdayName => {
let weekday = try_consume!(scan::short_or_long_weekday(s)); 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 => { &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) { let ampm = match (s.as_bytes()[0] | 32, s.as_bytes()[1] | 32) {
(b'a',b'm') => false, (b'a',b'm') => false,
(b'p',b'm') => true, (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..]; s = &s[2..];
} }
&Nanosecond | &Nanosecond3 | &Nanosecond6 | &Nanosecond9 => { &Nanosecond | &Nanosecond3 | &Nanosecond6 | &Nanosecond9 => {
if s.starts_with('.') { if s.starts_with('.') {
let nano = try_consume!(scan::nanosecond(&s[1..])); 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 }) => { &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)); 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 }) => { &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)); 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 }) => { &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)); 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 => { &TimezoneOffsetColon | &TimezoneOffset => {
let offset = try_consume!(scan::timezone_offset(s.trim_left(), let offset = try_consume!(scan::timezone_offset(s.trim_left(),
scan::colon_or_space)); scan::colon_or_space));
parsed.set_offset(i64::from(offset))?; parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?;
} }
&TimezoneOffsetColonZ | &TimezoneOffsetZ => { &TimezoneOffsetColonZ | &TimezoneOffsetZ => {
let offset = try_consume!(scan::timezone_offset_zulu(s.trim_left(), let offset = try_consume!(scan::timezone_offset_zulu(s.trim_left(),
scan::colon_or_space)); 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 }) => { &Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive }) => {
let offset = try_consume!(scan::timezone_offset_permissive( let offset = try_consume!(scan::timezone_offset_permissive(
s.trim_left(), scan::colon_or_space)); 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)), &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 => { &Item::Error => {
return Err(BAD_FORMAT); return Err((s, BAD_FORMAT));
} }
} }
} }
// if there are trailling chars, it is an error // if there are trailling chars, it is an error
if !s.is_empty() { if !s.is_empty() {
Err(TOO_LONG) Err((s, TOO_LONG))
} else { } else {
Ok(()) Ok(s)
}
}
impl str::FromStr for DateTime<FixedOffset> {
type Err = ParseError;
fn from_str(s: &str) -> ParseResult<DateTime<FixedOffset>> {
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()
} }
} }

View File

@ -262,8 +262,20 @@ pub fn timezone_offset_zulu<F>(s: &str, colon: F)
-> ParseResult<(&str, i32)> -> ParseResult<(&str, i32)>
where F: FnMut(&str) -> ParseResult<&str> 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'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), _ => timezone_offset(s, colon),
} }
} }