From 76b087372246443cbb51099837c43a66029c22ec Mon Sep 17 00:00:00 2001 From: Kang Seonghoon Date: Wed, 18 Feb 2015 23:27:12 +0900 Subject: [PATCH] added `FromStr` impls to naive date and time types. also, previously `Numeric::Nanosecond` had a special left-aligned parsing behavior. this commit replaces that with a newly designated `Fixed::Nanosecond` which also handles an empty string which is possible with an integral number of seconds. --- src/format/mod.rs | 18 ++++++ src/format/parse.rs | 127 +++++++++++++++++++++++++----------------- src/format/scan.rs | 36 +++++++----- src/naive/date.rs | 84 +++++++++++++++++++++++----- src/naive/datetime.rs | 68 +++++++++++++++++++++- src/naive/time.rs | 65 ++++++++++++++++++++- 6 files changed, 313 insertions(+), 85 deletions(-) diff --git a/src/format/mod.rs b/src/format/mod.rs index a5be021..9605474 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -84,6 +84,7 @@ pub enum Numeric { /// The number of seconds since the last whole minute (FW=PW=2). Second, /// The number of nanoseconds since the last whole second (FW=PW=9). + /// Note that this is *not* left-aligned; see also `Fixed::Nanosecond`. Nanosecond, /// The number of non-leap seconds since January 1, 1970 0:00:00 UTC (FW=1, PW=infinity). Timestamp, @@ -119,6 +120,10 @@ pub enum Fixed { /// /// Prints in upper case, reads in any case. UpperAmPm, + /// An optional dot plus one or more digits for left-aligned nanoseconds. + /// May print nothing, 3, 6 or 9 digits according to the available accuracy. + /// See also `Numeric::Nanosecond`. + Nanosecond, /// Timezone name. /// /// It does not support parsing, its use in the parser is an immediate failure. @@ -340,6 +345,19 @@ pub fn format<'a, I>(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Opt time.map(|t| write!(w, "{}", if t.hour12().0 {"pm"} else {"am"})), UpperAmPm => time.map(|t| write!(w, "{}", if t.hour12().0 {"PM"} else {"AM"})), + Nanosecond => + time.map(|t| { + let nano = t.nanosecond() % 1_000_000_000; + if nano == 0 { + Ok(()) + } else if nano % 1_000_000 == 0 { + write!(w, ".{:03}", nano / 1_000_000) + } else if nano % 1_000 == 0 { + write!(w, ".{:06}", nano / 1_000) + } else { + write!(w, ".{:09}", nano) + } + }), TimezoneName => off.map(|&(ref name, _)| write!(w, "{}", *name)), TimezoneOffset => diff --git a/src/format/parse.rs b/src/format/parse.rs index 62cf9cc..b5f7512 100644 --- a/src/format/parse.rs +++ b/src/format/parse.rs @@ -95,14 +95,14 @@ fn parse_rfc2822<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st } s = s.trim_left(); - try!(parsed.set_day(try_consume!(scan::number(s, 1, 2, false)))); + try!(parsed.set_day(try_consume!(scan::number(s, 1, 2)))); s = try!(scan::space(s)); // mandatory try!(parsed.set_month(1 + try_consume!(scan::short_month0(s)) as i64)); s = try!(scan::space(s)); // mandatory // distinguish two- and three-digit years from four-digit years let prevlen = s.len(); - let mut year = try_consume!(scan::number(s, 2, usize::MAX, false)); + let mut year = try_consume!(scan::number(s, 2, usize::MAX)); let yearlen = prevlen - s.len(); match (yearlen, year) { (2, 0...49) => { year += 2000; } // 47 -> 2047, 05 -> 2005 @@ -113,13 +113,13 @@ fn parse_rfc2822<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st try!(parsed.set_year(year)); s = try!(scan::space(s)); // mandatory - try!(parsed.set_hour(try_consume!(scan::number(s, 2, 2, false)))); + try!(parsed.set_hour(try_consume!(scan::number(s, 2, 2)))); s = try!(scan::char(s.trim_left(), b':')).trim_left(); // *S ":" *S - try!(parsed.set_minute(try_consume!(scan::number(s, 2, 2, false)))); + try!(parsed.set_minute(try_consume!(scan::number(s, 2, 2)))); s = s.trim_left(); if !s.is_empty() { // [ ":" *S 2DIGIT ] s = try!(scan::char(s, b':')).trim_left(); - try!(parsed.set_second(try_consume!(scan::number(s, 2, 2, false)))); + try!(parsed.set_second(try_consume!(scan::number(s, 2, 2)))); } s = try!(scan::space(s)); // mandatory @@ -163,11 +163,11 @@ fn parse_rfc3339<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st // note that this restriction is unique to RFC 3339 and not ISO 8601. // since this is not a typical Chrono behavior, we check it earlier. - try!(parsed.set_year(try_consume!(scan::number(s, 4, 4, false)))); + try!(parsed.set_year(try_consume!(scan::number(s, 4, 4)))); s = try!(scan::char(s, b'-')); - try!(parsed.set_month(try_consume!(scan::number(s, 2, 2, false)))); + try!(parsed.set_month(try_consume!(scan::number(s, 2, 2)))); s = try!(scan::char(s, b'-')); - try!(parsed.set_day(try_consume!(scan::number(s, 2, 2, false)))); + try!(parsed.set_day(try_consume!(scan::number(s, 2, 2)))); s = match s.as_bytes().first() { Some(&b't') | Some(&b'T') => &s[1..], @@ -175,14 +175,13 @@ fn parse_rfc3339<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st None => return Err(TOO_SHORT), }; - try!(parsed.set_hour(try_consume!(scan::number(s, 2, 2, false)))); + try!(parsed.set_hour(try_consume!(scan::number(s, 2, 2)))); s = try!(scan::char(s, b':')); - try!(parsed.set_minute(try_consume!(scan::number(s, 2, 2, false)))); + try!(parsed.set_minute(try_consume!(scan::number(s, 2, 2)))); s = try!(scan::char(s, b':')); - try!(parsed.set_second(try_consume!(scan::number(s, 2, 2, false)))); + try!(parsed.set_second(try_consume!(scan::number(s, 2, 2)))); if s.starts_with(".") { - let nanosecond = try_consume!(scan::number(&s[1..], 1, 9, true)); - s = s.trim_left_matches(|c: char| '0' <= c && c <= '9'); + let nanosecond = try_consume!(scan::nanosecond(&s[1..])); try!(parsed.set_nanosecond(nanosecond)); } @@ -226,47 +225,43 @@ pub fn parse<'a, I>(parsed: &mut Parsed, mut s: &str, items: I) -> ParseResult<( Item::Numeric(spec, _pad) => { use super::Numeric::*; - enum Mode { Right, Left, SignedRight } - - let (width, mode, set): (usize, Mode, - fn(&mut Parsed, i64) -> ParseResult<()>) = match spec { - Year => (4, Mode::SignedRight, Parsed::set_year), - YearDiv100 => (2, Mode::Right, Parsed::set_year_div_100), - YearMod100 => (2, Mode::Right, Parsed::set_year_mod_100), - IsoYear => (4, Mode::SignedRight, Parsed::set_isoyear), - IsoYearDiv100 => (2, Mode::Right, Parsed::set_isoyear_div_100), - IsoYearMod100 => (2, Mode::Right, Parsed::set_isoyear_mod_100), - Month => (2, Mode::Right, Parsed::set_month), - Day => (2, Mode::Right, Parsed::set_day), - WeekFromSun => (2, Mode::Right, Parsed::set_week_from_sun), - WeekFromMon => (2, Mode::Right, Parsed::set_week_from_mon), - IsoWeek => (2, Mode::Right, Parsed::set_isoweek), - NumDaysFromSun => (1, Mode::Right, set_weekday_with_num_days_from_sunday), - WeekdayFromMon => (1, Mode::Right, set_weekday_with_number_from_monday), - Ordinal => (3, Mode::Right, Parsed::set_ordinal), - Hour => (2, Mode::Right, Parsed::set_hour), - Hour12 => (2, Mode::Right, Parsed::set_hour12), - Minute => (2, Mode::Right, Parsed::set_minute), - Second => (2, Mode::Right, Parsed::set_second), - Nanosecond => (9, Mode::Left, Parsed::set_nanosecond), - Timestamp => (usize::MAX, Mode::Right, Parsed::set_timestamp), + let (width, signed, set): (usize, bool, + fn(&mut Parsed, i64) -> ParseResult<()>) = match spec { + Year => (4, true, Parsed::set_year), + YearDiv100 => (2, false, Parsed::set_year_div_100), + YearMod100 => (2, false, Parsed::set_year_mod_100), + IsoYear => (4, true, Parsed::set_isoyear), + IsoYearDiv100 => (2, false, Parsed::set_isoyear_div_100), + IsoYearMod100 => (2, false, Parsed::set_isoyear_mod_100), + Month => (2, false, Parsed::set_month), + Day => (2, false, Parsed::set_day), + WeekFromSun => (2, false, Parsed::set_week_from_sun), + WeekFromMon => (2, false, Parsed::set_week_from_mon), + IsoWeek => (2, false, Parsed::set_isoweek), + NumDaysFromSun => (1, false, set_weekday_with_num_days_from_sunday), + WeekdayFromMon => (1, false, set_weekday_with_number_from_monday), + Ordinal => (3, false, Parsed::set_ordinal), + Hour => (2, false, Parsed::set_hour), + Hour12 => (2, false, Parsed::set_hour12), + Minute => (2, false, Parsed::set_minute), + Second => (2, false, Parsed::set_second), + Nanosecond => (9, false, Parsed::set_nanosecond), + Timestamp => (usize::MAX, false, Parsed::set_timestamp), }; s = s.trim_left(); - let v = match mode { - Mode::Right => try_consume!(scan::number(s, 1, width, false)), - Mode::Left => try_consume!(scan::number(s, 1, width, true)), - Mode::SignedRight => { - if s.starts_with("-") { - let v = try_consume!(scan::number(&s[1..], 1, usize::MAX, false)); - try!(0i64.checked_sub(v).ok_or(OUT_OF_RANGE)) - } else if s.starts_with("+") { - try_consume!(scan::number(&s[1..], 1, usize::MAX, false)) - } else { - // if there is no explicit sign, we respect the original `width` - try_consume!(scan::number(s, 1, width, false)) - } - }, + let v = if signed { + if s.starts_with("-") { + let v = try_consume!(scan::number(&s[1..], 1, usize::MAX)); + try!(0i64.checked_sub(v).ok_or(OUT_OF_RANGE)) + } else if s.starts_with("+") { + try_consume!(scan::number(&s[1..], 1, usize::MAX)) + } else { + // if there is no explicit sign, we respect the original `width` + try_consume!(scan::number(s, 1, width)) + } + } else { + try_consume!(scan::number(s, 1, width)) }; try!(set(parsed, v)); } @@ -306,6 +301,13 @@ pub fn parse<'a, I>(parsed: &mut Parsed, mut s: &str, items: I) -> ParseResult<( s = &s[2..]; } + Nanosecond => { + if s.starts_with(".") { + let nano = try_consume!(scan::nanosecond(&s[1..])); + try!(parsed.set_nanosecond(nano)); + } + } + TimezoneName => return Err(BAD_FORMAT), TimezoneOffset => { @@ -447,7 +449,7 @@ fn test_parse() { weekday: Weekday::Sun, ordinal: 89, hour_mod_12: 1); check!("23 45 6 78901234 567890123", [num!(Hour), num!(Minute), num!(Second), num!(Nanosecond), num!(Timestamp)]; - hour_div_12: 1, hour_mod_12: 11, minute: 45, second: 6, nanosecond: 789_012_340, + hour_div_12: 1, hour_mod_12: 11, minute: 45, second: 6, nanosecond: 78_901_234, timestamp: 567_890_123); // fixed: month and weekday names @@ -500,6 +502,24 @@ fn test_parse() { check!("xx", [fix!(LowerAmPm)]; INVALID); check!("", [fix!(LowerAmPm)]; TOO_SHORT); + // fixed: dot plus nanoseconds + check!("", [fix!(Nanosecond)]; ); // no field set, but not an error + check!("4", [fix!(Nanosecond)]; TOO_LONG); // never consumes `4` + check!("4", [fix!(Nanosecond), num!(Second)]; second: 4); + check!(".0", [fix!(Nanosecond)]; nanosecond: 0); + check!(".4", [fix!(Nanosecond)]; nanosecond: 400_000_000); + check!(".42", [fix!(Nanosecond)]; nanosecond: 420_000_000); + check!(".421", [fix!(Nanosecond)]; nanosecond: 421_000_000); + check!(".42195", [fix!(Nanosecond)]; nanosecond: 421_950_000); + check!(".421950803", [fix!(Nanosecond)]; nanosecond: 421_950_803); + check!(".421950803547", [fix!(Nanosecond)]; nanosecond: 421_950_803); + check!(".000000003547", [fix!(Nanosecond)]; nanosecond: 3); + check!(".000000000547", [fix!(Nanosecond)]; nanosecond: 0); + check!(".", [fix!(Nanosecond)]; TOO_SHORT); + check!(".4x", [fix!(Nanosecond)]; TOO_LONG); + check!(". 4", [fix!(Nanosecond)]; INVALID); + check!(" .4", [fix!(Nanosecond)]; TOO_LONG); // no automatic trimming + // fixed: timezone offsets check!("+00:00", [fix!(TimezoneOffset)]; offset: 0); check!("-00:00", [fix!(TimezoneOffset)]; offset: 0); @@ -560,6 +580,9 @@ fn test_parse() { hour_div_12: 1, hour_mod_12: 3, minute: 14); check!("12345678901234.56789", [num!(Timestamp), lit!("."), num!(Nanosecond)]; + nanosecond: 56_789, timestamp: 12_345_678_901_234); + check!("12345678901234.56789", + [num!(Timestamp), fix!(Nanosecond)]; nanosecond: 567_890_000, timestamp: 12_345_678_901_234); } diff --git a/src/format/scan.rs b/src/format/scan.rs index 923e648..b3f4d7f 100644 --- a/src/format/scan.rs +++ b/src/format/scan.rs @@ -21,15 +21,10 @@ fn equals(s: &str, pattern: &str) -> bool { /// Tries to parse the non-negative number from `min` to `max` digits. /// -/// If `left_aligned` is true, the number is assumed to be followed by zero digits -/// so that the padded digits are exactly `max` digits long (like fractions). -/// For example, given `max` of 8, `123` is parsed as 123 when right-aligned -/// and 12300000 when left-aligned. -/// /// The absence of digits at all is an unconditional error. /// More than `max` digits are consumed up to the first `max` digits. /// Any number that does not fit in `i64` is an error. -pub fn number(s: &str, min: usize, max: usize, left_aligned: bool) -> ParseResult<(&str, i64)> { +pub fn number(s: &str, min: usize, max: usize) -> ParseResult<(&str, i64)> { assert!(min <= max); // limit `s` to given number of digits @@ -43,18 +38,29 @@ pub fn number(s: &str, min: usize, max: usize, left_aligned: bool) -> ParseResul } // we can overflow here, which is the only possible cause of error from `parse`. - let mut v: i64 = try!(s[..upto].parse().map_err(|_| OUT_OF_RANGE)); - - // scale the number if it is left-aligned. this can also overflow. - if left_aligned { - for _ in upto..max { - v = try!(v.checked_mul(10).ok_or(OUT_OF_RANGE)); - } - } - + let v: i64 = try!(s[..upto].parse().map_err(|_| OUT_OF_RANGE)); Ok((&s[upto..], v)) } +/// Tries to consume at least one digits as a fractional second. +/// Returns the number of whole nanoseconds (0--999,999,999). +pub fn nanosecond(s: &str) -> ParseResult<(&str, i64)> { + // record the number of digits consumed for later scaling. + let origlen = s.len(); + let (s, v) = try!(number(s, 1, 9)); + let consumed = origlen - s.len(); + + // scale the number accordingly. + static SCALE: [i64; 10] = [0, 100_000_000, 10_000_000, 1_000_000, 100_000, 10_000, + 1_000, 100, 10, 1]; + let v = try!(v.checked_mul(SCALE[consumed]).ok_or(OUT_OF_RANGE)); + + // if there are more than 9 digits, skip next digits. + let s = s.trim_left_matches(|c: char| '0' <= c && c <= '9'); + + Ok((s, v)) +} + /// Tries to parse the month index (0 through 11) with the first three ASCII letters. pub fn short_month0(s: &str) -> ParseResult<(&str, u8)> { if s.len() < 3 { return Err(TOO_SHORT); } diff --git a/src/naive/date.rs b/src/naive/date.rs index 791330b..2d62220 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -6,7 +6,7 @@ * ISO 8601 calendar date without timezone. */ -use std::{fmt, hash}; +use std::{str, fmt, hash}; use std::num::{Int, ToPrimitive}; use std::ops::{Add, Sub}; @@ -15,7 +15,8 @@ use div::div_mod_floor; use duration::Duration; use naive::time::NaiveTime; use naive::datetime::NaiveDateTime; -use format::{parse, Item, Parsed, ParseResult, DelayedFormat, StrftimeItems}; +use format::{Item, Numeric, Pad}; +use format::{parse, Parsed, ParseError, ParseResult, DelayedFormat, StrftimeItems}; use self::internals::{DateImpl, Of, Mdf, YearFlags}; @@ -501,14 +502,25 @@ impl fmt::Debug for NaiveDate { } impl fmt::Display for NaiveDate { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let year = self.year(); - let mdf = self.mdf(); - if year >= 0 { - write!(f, "{:04}-{:02}-{:02}", year, mdf.month(), mdf.day()) - } else { - write!(f, "{:+05}-{:02}-{:02}", year, mdf.month(), mdf.day()) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(self, f) } +} + +impl str::FromStr for NaiveDate { + type Err = ParseError; + + fn from_str(s: &str) -> ParseResult { + const ITEMS: &'static [Item<'static>] = &[ + Item::Space(""), Item::Numeric(Numeric::Year, Pad::Zero), + Item::Space(""), Item::Literal("-"), + Item::Space(""), Item::Numeric(Numeric::Month, Pad::Zero), + Item::Space(""), Item::Literal("-"), + Item::Space(""), Item::Numeric(Numeric::Day, Pad::Zero), + Item::Space(""), + ]; + + let mut parsed = Parsed::new(); + try!(parse(&mut parsed, s, ITEMS.iter().cloned())); + parsed.to_naive_date() } } @@ -835,16 +847,60 @@ mod tests { assert_eq!(format!("{:?}", NaiveDate::from_ymd(-307, 3, 4)), "-0307-03-04"); assert_eq!(format!("{:?}", NaiveDate::from_ymd(12345, 3, 4)), "+12345-03-04"); - assert_eq!(NaiveDate::from_ymd(2012, 3, 4).to_string(), "2012-03-04"); - assert_eq!(NaiveDate::from_ymd(0, 3, 4).to_string(), "0000-03-04"); - assert_eq!(NaiveDate::from_ymd(-307, 3, 4).to_string(), "-0307-03-04"); - assert_eq!(NaiveDate::from_ymd(12345, 3, 4).to_string(), "12345-03-04"); + assert_eq!(NaiveDate::from_ymd(2012, 3, 4).to_string(), "2012-03-04"); + assert_eq!(NaiveDate::from_ymd(0, 3, 4).to_string(), "0000-03-04"); + assert_eq!(NaiveDate::from_ymd(-307, 3, 4).to_string(), "-0307-03-04"); + assert_eq!(NaiveDate::from_ymd(12345, 3, 4).to_string(), "+12345-03-04"); // the format specifier should have no effect on `NaiveTime` assert_eq!(format!("{:+30?}", NaiveDate::from_ymd(1234, 5, 6)), "1234-05-06"); assert_eq!(format!("{:30?}", NaiveDate::from_ymd(12345, 6, 7)), "+12345-06-07"); } + #[test] + fn test_date_from_str() { + // valid cases + let valid = [ + "-0000000123456-1-2", + " -123456 - 1 - 2 ", + "-12345-1-2", + "-1234-12-31", + "-7-6-5", + "350-2-28", + "360-02-29", + "0360-02-29", + "2015-2 -18", + "+70-2-18", + "+70000-2-18", + "+00007-2-18", + ]; + for &s in &valid { + let d = match s.parse::() { + Ok(d) => d, + Err(e) => panic!("parsing `{}` has failed: {}", s, e) + }; + let s_ = format!("{:?}", d); + // `s` and `s_` may differ, but `s.parse()` and `s_.parse()` must be same + let d_ = match s_.parse::() { + Ok(d) => d, + Err(e) => panic!("`{}` is parsed into `{:?}`, but reparsing that has failed: {}", + s, d, e) + }; + assert!(d == d_, "`{}` is parsed into `{:?}`, but reparsed result \ + `{:?}` does not match", s, d, d_); + } + + // some invalid cases + // since `ParseErrorKind` is private, all we can do is to check if there was an error + assert!("".parse::().is_err()); + assert!("x".parse::().is_err()); + assert!("2014".parse::().is_err()); + assert!("2014-01".parse::().is_err()); + assert!("2014-01-00".parse::().is_err()); + assert!("2014-13-57".parse::().is_err()); + assert!("9999999-9-9".parse::().is_err()); // out-of-bounds + } + #[test] fn test_date_parse_from_str() { let ymd = |&: y,m,d| NaiveDate::from_ymd(y,m,d); diff --git a/src/naive/datetime.rs b/src/naive/datetime.rs index a3b3c14..5374256 100644 --- a/src/naive/datetime.rs +++ b/src/naive/datetime.rs @@ -6,7 +6,7 @@ * ISO 8601 date and time without timezone. */ -use std::{fmt, hash}; +use std::{str, fmt, hash}; use std::num::{Int, ToPrimitive}; use std::ops::{Add, Sub}; @@ -15,7 +15,8 @@ use div::div_mod_floor; use duration::Duration; use naive::time::NaiveTime; use naive::date::NaiveDate; -use format::{parse, Item, Parsed, ParseResult, DelayedFormat, StrftimeItems}; +use format::{Item, Numeric, Pad, Fixed}; +use format::{parse, Parsed, ParseError, ParseResult, DelayedFormat, StrftimeItems}; /// ISO 8601 combined date and time without timezone. #[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] @@ -264,6 +265,31 @@ impl fmt::Display for NaiveDateTime { } } +impl str::FromStr for NaiveDateTime { + type Err = ParseError; + + fn from_str(s: &str) -> ParseResult { + const ITEMS: &'static [Item<'static>] = &[ + Item::Space(""), Item::Numeric(Numeric::Year, Pad::Zero), + Item::Space(""), Item::Literal("-"), + Item::Space(""), Item::Numeric(Numeric::Month, Pad::Zero), + Item::Space(""), Item::Literal("-"), + Item::Space(""), Item::Numeric(Numeric::Day, Pad::Zero), + Item::Space(""), Item::Literal("T"), // XXX shouldn't this be case-insensitive? + Item::Space(""), Item::Numeric(Numeric::Hour, Pad::Zero), + Item::Space(""), Item::Literal(":"), + Item::Space(""), Item::Numeric(Numeric::Minute, Pad::Zero), + Item::Space(""), Item::Literal(":"), + Item::Space(""), Item::Numeric(Numeric::Second, Pad::Zero), + Item::Fixed(Fixed::Nanosecond), Item::Space(""), + ]; + + let mut parsed = Parsed::new(); + try!(parse(&mut parsed, s, ITEMS.iter().cloned())); + parsed.to_naive_datetime_with_offset(0) + } +} + #[cfg(test)] mod tests { use super::NaiveDateTime; @@ -344,6 +370,44 @@ mod tests { assert_eq!(to_timestamp(2038, 1, 19, 3, 14, 7), 0x7fffffff); } + #[test] + fn test_datetime_from_str() { + // valid cases + let valid = [ + "2015-2-18T23:16:9.15", + "-77-02-18T23:16:09", + " +82701 - 05 - 6 T 15 : 9 : 60.898989898989 ", + ]; + for &s in &valid { + let d = match s.parse::() { + Ok(d) => d, + Err(e) => panic!("parsing `{}` has failed: {}", s, e) + }; + let s_ = format!("{:?}", d); + // `s` and `s_` may differ, but `s.parse()` and `s_.parse()` must be same + let d_ = match s_.parse::() { + Ok(d) => d, + Err(e) => panic!("`{}` is parsed into `{:?}`, but reparsing that has failed: {}", + s, d, e) + }; + assert!(d == d_, "`{}` is parsed into `{:?}`, but reparsed result \ + `{:?}` does not match", s, d, d_); + } + + // some invalid cases + // since `ParseErrorKind` is private, all we can do is to check if there was an error + assert!("".parse::().is_err()); + assert!("x".parse::().is_err()); + assert!("15".parse::().is_err()); + assert!("15:8:9".parse::().is_err()); + assert!("15-8-9".parse::().is_err()); + assert!("2015-15-15T15:15:15".parse::().is_err()); + assert!("2012-12-12T12:12:12x".parse::().is_err()); + assert!("2012-123-12T12:12:12".parse::().is_err()); + assert!("+ 82701-123-12T12:12:12".parse::().is_err()); + assert!("+802701-123-12T12:12:12".parse::().is_err()); // out-of-bound + } + #[test] fn test_datetime_parse_from_str() { let ymdhms = |&: y,m,d,h,n,s| NaiveDate::from_ymd(y,m,d).and_hms(h,n,s); diff --git a/src/naive/time.rs b/src/naive/time.rs index 7dbb103..e70a605 100644 --- a/src/naive/time.rs +++ b/src/naive/time.rs @@ -6,7 +6,7 @@ * ISO 8601 time without timezone. */ -use std::{fmt, hash}; +use std::{str, fmt, hash}; use std::num::Int; use std::ops::{Add, Sub}; @@ -14,7 +14,8 @@ use Timelike; use div::div_mod_floor; use offset::Offset; use duration::Duration; -use format::{parse, Item, Parsed, ParseResult, DelayedFormat, StrftimeItems}; +use format::{Item, Numeric, Pad, Fixed}; +use format::{parse, Parsed, ParseError, ParseResult, DelayedFormat, StrftimeItems}; /// ISO 8601 time without timezone. /// Allows for the nanosecond precision and optional leap second representation. @@ -264,6 +265,25 @@ impl fmt::Display for NaiveTime { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(self, f) } } +impl str::FromStr for NaiveTime { + type Err = ParseError; + + fn from_str(s: &str) -> ParseResult { + const ITEMS: &'static [Item<'static>] = &[ + Item::Space(""), Item::Numeric(Numeric::Hour, Pad::Zero), + Item::Space(""), Item::Literal(":"), + Item::Space(""), Item::Numeric(Numeric::Minute, Pad::Zero), + Item::Space(""), Item::Literal(":"), + Item::Space(""), Item::Numeric(Numeric::Second, Pad::Zero), + Item::Fixed(Fixed::Nanosecond), Item::Space(""), + ]; + + let mut parsed = Parsed::new(); + try!(parse(&mut parsed, s, ITEMS.iter().cloned())); + parsed.to_naive_time() + } +} + #[cfg(test)] mod tests { use super::NaiveTime; @@ -383,6 +403,47 @@ mod tests { assert_eq!(format!("{:30}", NaiveTime::from_hms_milli(3, 5, 7, 9)), "03:05:07.009"); } + #[test] + fn test_date_from_str() { + // valid cases + let valid = [ + "0:0:0", + "0:0:0.0000000", + "0:0:0.0000003", + " 4 : 3 : 2.1 ", + " 09:08:07 ", + " 9:8:07 ", + "23:59:60.373929310237", + ]; + for &s in &valid { + let d = match s.parse::() { + Ok(d) => d, + Err(e) => panic!("parsing `{}` has failed: {}", s, e) + }; + let s_ = format!("{:?}", d); + // `s` and `s_` may differ, but `s.parse()` and `s_.parse()` must be same + let d_ = match s_.parse::() { + Ok(d) => d, + Err(e) => panic!("`{}` is parsed into `{:?}`, but reparsing that has failed: {}", + s, d, e) + }; + assert!(d == d_, "`{}` is parsed into `{:?}`, but reparsed result \ + `{:?}` does not match", s, d, d_); + } + + // some invalid cases + // since `ParseErrorKind` is private, all we can do is to check if there was an error + assert!("".parse::().is_err()); + assert!("x".parse::().is_err()); + assert!("15".parse::().is_err()); + assert!("15:8".parse::().is_err()); + assert!("15:8:x".parse::().is_err()); + assert!("15:8:9x".parse::().is_err()); + assert!("23:59:61".parse::().is_err()); + assert!("12:34:56.x".parse::().is_err()); + assert!("12:34:56. 0".parse::().is_err()); + } + #[test] fn test_time_parse_from_str() { let hms = |&: h,m,s| NaiveTime::from_hms(h,m,s);