Skip to content

Commit

Permalink
Merge pull request #378 from quodlibetor/round-trip-display
Browse files Browse the repository at this point in the history
support round tripping display <-> datetime
  • Loading branch information
quodlibetor committed Dec 30, 2019
2 parents b9cd0ce + b9c967b commit 4ad95ee
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 62 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
57 changes: 29 additions & 28 deletions src/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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>>(),
Expand Down Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions src/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
120 changes: 87 additions & 33 deletions src/format/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()..];
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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) => {
Expand All @@ -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)),
Expand All @@ -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()
}
}

Expand Down
14 changes: 13 additions & 1 deletion src/format/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
Expand Down

0 comments on commit 4ad95ee

Please sign in to comment.