Merge pull request #378 from quodlibetor/round-trip-display

support round tripping display <-> datetime
This commit is contained in:
Brandon W Maister 2019-12-30 17:15:51 -05:00 committed by GitHub
commit 4ad95ee602
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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
* 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
#102)

View File

@ -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<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> {
type Err = ParseError;
@ -2104,6 +2077,15 @@ mod tests {
#[test]
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>>(),
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>>(),
@ -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<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]
#[cfg(feature="clock")]
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)]
pub struct ParseError(ParseErrorKind);
/// The category of parse error
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
enum ParseErrorKind {
/// Given field is out of permitted range.

View File

@ -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<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 {
($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<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)>
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),
}
}