diff --git a/src/datetime.rs b/src/datetime.rs index 007a10b..d784f44 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -11,12 +11,12 @@ use std::cmp::Ordering; use std::ops::{Add, Sub}; use {Weekday, Timelike, Datelike}; -use offset::Offset; +use offset::{Offset, FixedOffset}; use duration::Duration; use naive::datetime::NaiveDateTime; use time::Time; use date::Date; -use format::{DelayedFormat, StrftimeItems}; +use format::{parse, Parsed, ParseResult, DelayedFormat, StrftimeItems}; /// ISO 8601 combined date and time with timezone. #[derive(Clone)] @@ -88,6 +88,19 @@ impl DateTime { } } +impl DateTime { + /// Parses a string with the specified format string and + /// returns a new `DateTime` with a parsed `FixedOffset`. + /// See the `format::strftime` module on the supported escape sequences. + /// + /// See also `Offset::datetime_from_str` which gives a local `DateTime` on specific time zone. + pub fn from_str(s: &str, fmt: &str) -> ParseResult> { + let mut parsed = Parsed::new(); + try!(parse(&mut parsed, s, StrftimeItems::new(fmt))); + parsed.to_datetime() + } +} + impl DateTime { /// Formats the combined date and time in the specified format string. /// See the `format::strftime` module on the supported escape sequences. @@ -244,7 +257,8 @@ impl fmt::Display for DateTime { #[cfg(test)] mod tests { - use {Datelike}; + use super::DateTime; + use Datelike; use duration::Duration; use offset::{Offset, UTC, Local, FixedOffset}; @@ -275,7 +289,20 @@ mod tests { } #[test] - fn test_datetime_fmt_with_local() { + fn test_datetime_from_str() { + let ymdhms = |&: y,m,d,h,n,s,off| FixedOffset::east(off).ymd(y,m,d).and_hms(h,n,s); + assert_eq!(DateTime::from_str("2014-5-7T12:34:56+09:30", "%Y-%m-%dT%H:%M:%S%z"), + Ok(ymdhms(2014, 5, 7, 12, 34, 56, 570*60))); // ignore offset + assert!(DateTime::from_str("20140507000000", "%Y%m%d%H%M%S").is_err()); // no offset + assert!(DateTime::from_str("Fri, 09 Aug 2013 23:54:35 GMT", + "%a, %d %b %Y %H:%M:%S GMT").is_err()); + assert_eq!(UTC.datetime_from_str("Fri, 09 Aug 2013 23:54:35 GMT", + "%a, %d %b %Y %H:%M:%S GMT"), + Ok(UTC.ymd(2013, 8, 9).and_hms(23, 54, 35))); + } + + #[test] + fn test_datetime_format_with_local() { // if we are not around the year boundary, local and UTC date should have the same year let dt = Local::now().with_month(5).unwrap(); assert_eq!(dt.format("%Y").to_string(), dt.with_offset(UTC).format("%Y").to_string()); diff --git a/src/format/mod.rs b/src/format/mod.rs index 1884ece..8dfbf51 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -19,7 +19,7 @@ use offset::Offset; use naive::date::NaiveDate; use naive::time::NaiveTime; -use self::parsed::Parsed; +pub use self::parsed::Parsed; pub use self::strftime::StrftimeItems; /// Padding characters for numeric items. diff --git a/src/format/parsed.rs b/src/format/parsed.rs index c37294b..b7e8dab 100644 --- a/src/format/parsed.rs +++ b/src/format/parsed.rs @@ -491,6 +491,25 @@ impl Parsed { LocalResult::Ambiguous(..) => Err(NOT_ENOUGH), } } + + /// Returns a parsed timezone-aware date and time out of given fields, + /// with an additional `Offset` used to interpret and validate the local date. + /// + /// This method is able to determine the combined date and time + /// from date and time fields or a single `timestamp` field, plus a time zone offset. + /// Either way those fields have to be consistent to each other. + /// If parsed fields include an UTC offset, it also has to be consistent to `offset`. + pub fn to_datetime_with_offset(&self, offset: Off) -> ParseResult> { + let delta = offset.local_minus_utc().num_seconds(); + let delta = try!(delta.to_i32().ok_or(OUT_OF_RANGE)); + if self.offset.unwrap_or(delta) != delta { return Err(IMPOSSIBLE); } + let datetime = try!(self.to_naive_datetime_with_offset(delta)); + match offset.from_local_datetime(&datetime) { + LocalResult::None => Err(IMPOSSIBLE), + LocalResult::Single(t) => Ok(t), + LocalResult::Ambiguous(..) => Err(NOT_ENOUGH), + } + } } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 8218b0c..1a0686c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,6 +200,7 @@ pub use naive::datetime::NaiveDateTime; pub use date::Date; pub use time::Time; pub use datetime::DateTime; +pub use format::{ParseError, ParseResult}; // useful throughout the codebase macro_rules! try_opt { diff --git a/src/naive/date.rs b/src/naive/date.rs index ff5fcc1..a2efa5d 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -15,7 +15,7 @@ use div::div_mod_floor; use duration::Duration; use naive::time::NaiveTime; use naive::datetime::NaiveDateTime; -use format::{DelayedFormat, StrftimeItems}; +use format::{parse, Parsed, ParseResult, DelayedFormat, StrftimeItems}; use self::internals::{DateImpl, Of, Mdf, YearFlags}; @@ -180,6 +180,14 @@ impl NaiveDate { Of::new(ordinal, flags)) } + /// Parses a string with the specified format string and returns a new `NaiveDate`. + /// See the `format::strftime` module on the supported escape sequences. + pub fn from_str(s: &str, fmt: &str) -> ParseResult { + let mut parsed = Parsed::new(); + try!(parse(&mut parsed, s, StrftimeItems::new(fmt))); + parsed.to_naive_date() + } + /// Makes a new `NaiveDateTime` from the current date and given `NaiveTime`. #[inline] pub fn and_time(&self, time: NaiveTime) -> NaiveDateTime { @@ -830,6 +838,20 @@ mod tests { assert_eq!(format!("{:30?}", NaiveDate::from_ymd(12345, 6, 7)), "+12345-06-07"); } + #[test] + fn test_date_from_str() { + let ymd = |&: y,m,d| NaiveDate::from_ymd(y,m,d); + assert_eq!(NaiveDate::from_str("2014-5-7T12:34:56+09:30", "%Y-%m-%dT%H:%M:%S%z"), + Ok(ymd(2014, 5, 7))); // ignore time and offset + assert_eq!(NaiveDate::from_str("2015-W06-1=2015-033", "%G-W%V-%u = %Y-%j"), + Ok(ymd(2015, 2, 2))); + assert_eq!(NaiveDate::from_str("Fri, 09 Aug 13", "%a, %d %b %y"), + Ok(ymd(2013, 8, 9))); + assert!(NaiveDate::from_str("Sat, 09 Aug 2013", "%a, %d %b %Y").is_err()); + assert!(NaiveDate::from_str("2014-57", "%Y-%m-%d").is_err()); + assert!(NaiveDate::from_str("2014", "%Y").is_err()); // insufficient + } + #[test] fn test_date_format() { let d = NaiveDate::from_ymd(2012, 3, 4); diff --git a/src/naive/datetime.rs b/src/naive/datetime.rs index ced799f..2e9a5d9 100644 --- a/src/naive/datetime.rs +++ b/src/naive/datetime.rs @@ -15,7 +15,7 @@ use div::div_mod_floor; use duration::Duration; use naive::time::NaiveTime; use naive::date::NaiveDate; -use format::{DelayedFormat, StrftimeItems}; +use format::{parse, Parsed, ParseResult, DelayedFormat, StrftimeItems}; /// ISO 8601 combined date and time without timezone. #[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] @@ -60,6 +60,14 @@ impl NaiveDateTime { } } + /// Parses a string with the specified format string and returns a new `NaiveDateTime`. + /// See the `format::strftime` module on the supported escape sequences. + pub fn from_str(s: &str, fmt: &str) -> ParseResult { + let mut parsed = Parsed::new(); + try!(parse(&mut parsed, s, StrftimeItems::new(fmt))); + parsed.to_naive_datetime_with_offset(0) // no offset adjustment + } + /// Retrieves a date component. #[inline] pub fn date(&self) -> NaiveDate { @@ -330,6 +338,22 @@ mod tests { assert_eq!(to_timestamp(2038, 1, 19, 3, 14, 7), 0x7fffffff); } + #[test] + fn test_datetime_from_str() { + let ymdhms = |&: y,m,d,h,n,s| NaiveDate::from_ymd(y,m,d).and_hms(h,n,s); + assert_eq!(NaiveDateTime::from_str("2014-5-7T12:34:56+09:30", "%Y-%m-%dT%H:%M:%S%z"), + Ok(ymdhms(2014, 5, 7, 12, 34, 56))); // ignore offset + assert_eq!(NaiveDateTime::from_str("2015-W06-1 000000", "%G-W%V-%u%H%M%S"), + Ok(ymdhms(2015, 2, 2, 0, 0, 0))); + assert_eq!(NaiveDateTime::from_str("Fri, 09 Aug 2013 23:54:35 GMT", + "%a, %d %b %Y %H:%M:%S GMT"), + Ok(ymdhms(2013, 8, 9, 23, 54, 35))); + assert!(NaiveDateTime::from_str("Sat, 09 Aug 2013 23:54:35 GMT", + "%a, %d %b %Y %H:%M:%S GMT").is_err()); + assert!(NaiveDateTime::from_str("2014-5-7 12:3456", "%Y-%m-%d %H:%M:%S").is_err()); + assert!(NaiveDateTime::from_str("12:34:56", "%H:%M:%S").is_err()); // insufficient + } + #[test] fn test_datetime_format() { let dt = NaiveDate::from_ymd(2010, 9, 8).and_hms_milli(7, 6, 54, 321); diff --git a/src/naive/time.rs b/src/naive/time.rs index 5488e59..f20319c 100644 --- a/src/naive/time.rs +++ b/src/naive/time.rs @@ -14,7 +14,7 @@ use Timelike; use div::div_mod_floor; use offset::Offset; use duration::Duration; -use format::{DelayedFormat, StrftimeItems}; +use format::{parse, Parsed, ParseResult, DelayedFormat, StrftimeItems}; /// ISO 8601 time without timezone. /// Allows for the nanosecond precision and optional leap second representation. @@ -118,6 +118,14 @@ impl NaiveTime { Some(NaiveTime { secs: secs, frac: nano }) } + /// Parses a string with the specified format string and returns a new `NaiveTime`. + /// See the `format::strftime` module on the supported escape sequences. + pub fn from_str(s: &str, fmt: &str) -> ParseResult { + let mut parsed = Parsed::new(); + try!(parse(&mut parsed, s, StrftimeItems::new(fmt))); + parsed.to_naive_time() + } + /// Formats the time in the specified format string. /// See the `format::strftime` module on the supported escape sequences. #[inline] @@ -368,6 +376,16 @@ mod tests { assert_eq!(format!("{:30}", NaiveTime::from_hms_milli(3, 5, 7, 9)), "03:05:07.009"); } + #[test] + fn test_time_from_str() { + let hms = |&: h,m,s| NaiveTime::from_hms(h,m,s); + assert_eq!(NaiveTime::from_str("2014-5-7T12:34:56+09:30", "%Y-%m-%dT%H:%M:%S%z"), + Ok(hms(12, 34, 56))); // ignore date and offset + assert_eq!(NaiveTime::from_str("PM 12:59", "%P %H:%M"), + Ok(hms(12, 59, 0))); + assert!(NaiveTime::from_str("12:3456", "%H:%M:%S").is_err()); + } + #[test] fn test_time_format() { let t = NaiveTime::from_hms_nano(3, 5, 7, 98765432); diff --git a/src/offset.rs b/src/offset.rs index 6d61a24..59f1632 100644 --- a/src/offset.rs +++ b/src/offset.rs @@ -18,6 +18,7 @@ use naive::datetime::NaiveDateTime; use date::Date; use time::Time; use datetime::DateTime; +use format::{parse, Parsed, ParseResult, StrftimeItems}; /// The conversion result from the local time to the timezone-aware datetime types. #[derive(Clone, PartialEq, Debug)] @@ -291,6 +292,20 @@ pub trait Offset: Clone + fmt::Debug { } } + /// Parses a string with the specified format string and + /// returns a `DateTime` with the current offset. + /// See the `format::strftime` module on the supported escape sequences. + /// + /// If the format does not include offsets, the current offset is assumed; + /// otherwise the input should have a matching UTC offset. + /// + /// See also `DateTime::from_str` which gives a local `DateTime` with parsed `FixedOffset`. + fn datetime_from_str(&self, s: &str, fmt: &str) -> ParseResult> { + let mut parsed = Parsed::new(); + try!(parse(&mut parsed, s, StrftimeItems::new(fmt))); + parsed.to_datetime_with_offset(self.clone()) + } + /// Returns the *current* offset from UTC to the local time. fn local_minus_utc(&self) -> Duration;