diff --git a/src/format/mod.rs b/src/format/mod.rs index ba338ff..7e6ffed 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -45,16 +45,17 @@ pub enum Pad { /// parsed with the same formatting items. #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum Numeric { - /// Full Gregorian year (FW=PW=4). + /// Full Gregorian year (FW=4, PW=infinity). + /// May accept years before 1 BCE or after 9999 CE, given an initial sign. Year, - /// Gregorian year divided by 100 (century number; FW=PW=2). - /// Always rounds towards minus infinity. + /// Gregorian year divided by 100 (century number; FW=PW=2). Implies the non-negative year. YearDiv100, /// Gregorian year modulo 100 (FW=PW=2). Cannot be negative. YearMod100, - /// Year in the ISO week date (FW=PW=4). + /// Year in the ISO week date (FW=4, PW=infinity). + /// May accept years before 1 BCE or after 9999 CE, given an initial sign. IsoYear, - /// Year in the ISO week date, divided by 100 (FW=PW=2). Always rounds towards minus infinity. + /// Year in the ISO week date, divided by 100 (FW=PW=2). Implies the non-negative year. IsoYearDiv100, /// Year in the ISO week date, modulo 100 (FW=PW=2). Cannot be negative. IsoYearMod100, @@ -285,10 +286,19 @@ pub fn format<'a, I>(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Opt }; if let Some(v) = v { - match pad { - Pad::None => try!(write!(w, "{}", v)), - Pad::Zero => try!(write!(w, "{:01$}", v, width)), - Pad::Space => try!(write!(w, "{:1$}", v, width)), + if (spec == Year || spec == IsoYear) && !(0 <= v && v < 10000) { + // non-four-digit years require an explicit sign as per ISO 8601 + match pad { + Pad::None => try!(write!(w, "{:+}", v)), + Pad::Zero => try!(write!(w, "{:+01$}", v, width)), + Pad::Space => try!(write!(w, "{:+1$}", v, width)), + } + } else { + match pad { + Pad::None => try!(write!(w, "{}", v)), + Pad::Zero => try!(write!(w, "{:01$}", v, width)), + Pad::Space => try!(write!(w, "{:1$}", v, width)), + } } } else { return Err(fmt::Error); // insufficient arguments for given format @@ -348,7 +358,7 @@ pub fn format<'a, I>(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Opt }, RFC3339 => // (almost) same to `%Y-%m-%dT%H:%M:%S.%f%z` if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) { - // reuse `Debug` impls which already prints ISO 8601 format. + // reuse `Debug` impls which already print ISO 8601 format. // this is faster in this way. try!(write!(w, "{:?}T{:?}", d, t)); Some(write_local_minus_utc(w, off, false, true)) diff --git a/src/format/parse.rs b/src/format/parse.rs index fb89b37..62cf9cc 100644 --- a/src/format/parse.rs +++ b/src/format/parse.rs @@ -8,6 +8,7 @@ */ use std::usize; +use std::num::Int; use Weekday; @@ -225,31 +226,48 @@ pub fn parse<'a, I>(parsed: &mut Parsed, mut s: &str, items: I) -> ParseResult<( Item::Numeric(spec, _pad) => { use super::Numeric::*; - let (width, frac, set): (usize, bool, + enum Mode { Right, Left, SignedRight } + + let (width, mode, set): (usize, Mode, fn(&mut Parsed, i64) -> ParseResult<()>) = match spec { - Year => (4, false, Parsed::set_year), - YearDiv100 => (2, false, Parsed::set_year_div_100), - YearMod100 => (2, false, Parsed::set_year_mod_100), - IsoYear => (4, false, 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, true, Parsed::set_nanosecond), - Timestamp => (usize::MAX, false, Parsed::set_timestamp), + 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 v = try_consume!(scan::number(s.trim_left(), 1, width, frac)); + 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)) + } + }, + }; try!(set(parsed, v)); } @@ -377,8 +395,6 @@ fn test_parse() { check!("9999", [num!(Year)]; year: 9999); check!(" \t987", [num!(Year)]; year: 987); check!("5", [num!(Year)]; year: 5); - check!("-42", [num!(Year)]; INVALID); // while `year` supports the negative year, - check!("+42", [num!(Year)]; INVALID); // the parser doesn't (for now). check!("5\0", [num!(Year)]; TOO_LONG); check!("\05", [num!(Year)]; INVALID); check!("", [num!(Year)]; TOO_SHORT); @@ -393,6 +409,28 @@ fn test_parse() { check!("1234xx1234", [num!(Year), lit!("x"), num!(Year)]; INVALID); check!("1234 x 1234", [num!(Year), lit!("x"), num!(Year)]; INVALID); + // signed numeric + check!("-42", [num!(Year)]; year: -42); + check!("+42", [num!(Year)]; year: 42); + check!("-0042", [num!(Year)]; year: -42); + check!("+0042", [num!(Year)]; year: 42); + check!("-42195", [num!(Year)]; year: -42195); + check!("+42195", [num!(Year)]; year: 42195); + check!(" -42195", [num!(Year)]; year: -42195); + check!(" +42195", [num!(Year)]; year: 42195); + check!(" - 42", [num!(Year)]; INVALID); + check!(" + 42", [num!(Year)]; INVALID); + check!("-", [num!(Year)]; TOO_SHORT); + check!("+", [num!(Year)]; TOO_SHORT); + + // unsigned numeric + check!("345", [num!(Ordinal)]; ordinal: 345); + check!("+345", [num!(Ordinal)]; INVALID); + check!("-345", [num!(Ordinal)]; INVALID); + check!(" 345", [num!(Ordinal)]; ordinal: 345); + check!(" +345", [num!(Ordinal)]; INVALID); + check!(" -345", [num!(Ordinal)]; INVALID); + // various numeric fields check!("1234 5678", [num!(Year), num!(IsoYear)];