diff --git a/src/format/mod.rs b/src/format/mod.rs index fc1e902..caecb2b 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -213,29 +213,22 @@ pub enum Fixed { } /// An opaque type representing fixed-format item types for internal uses only. +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InternalFixed { - _dummy: Void, + val: InternalInternal, } -impl Clone for InternalFixed { - fn clone(&self) -> Self { - match self._dummy {} - } -} - -impl PartialEq for InternalFixed { - fn eq(&self, _other: &InternalFixed) -> bool { - match self._dummy {} - } -} - -impl Eq for InternalFixed { -} - -impl fmt::Debug for InternalFixed { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "") - } +#[derive(Debug, Clone, PartialEq, Eq)] +enum InternalInternal { + /// Same as [`TimezoneOffsetColonZ`](#variant.TimezoneOffsetColonZ), but + /// allows missing minutes (per [ISO 8601][iso8601]). + /// + /// # Panics + /// + /// If you try to use this for printing. + /// + /// [iso8601]: https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC + TimezoneOffsetPermissive, } /// A single formatting item. This is used for both formatting and parsing. @@ -264,6 +257,7 @@ macro_rules! num { ($x:ident) => (Item::Numeric(Numeric::$x, Pad::None)) } macro_rules! num0 { ($x:ident) => (Item::Numeric(Numeric::$x, Pad::Zero)) } macro_rules! nums { ($x:ident) => (Item::Numeric(Numeric::$x, Pad::Space)) } macro_rules! fix { ($x:ident) => (Item::Fixed(Fixed::$x)) } +macro_rules! internal_fix { ($x:ident) => (Item::Fixed(Fixed::Internal(InternalFixed { val: InternalInternal::$x })))} /// An error from the `parse` function. #[derive(Debug, Clone, PartialEq, Eq, Copy)] @@ -491,6 +485,8 @@ pub fn format<'a, I>(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Opt off.map(|&(_, off)| write_local_minus_utc(w, off, false, false)), TimezoneOffsetZ => off.map(|&(_, off)| write_local_minus_utc(w, off, true, false)), + Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive }) => + panic!("Do not try to write %#z it is undefined"), RFC2822 => // same to `%a, %e %b %Y %H:%M:%S %z` if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) { let sec = t.second() + t.nanosecond() / 1_000_000_000; @@ -511,9 +507,6 @@ pub fn format<'a, I>(w: &mut fmt::Formatter, date: Option<&NaiveDate>, time: Opt } else { None }, - - // for the future expansion - Internal(ref int) => match int._dummy {}, }; match ret { @@ -609,4 +602,3 @@ impl FromStr for Weekday { } } } - diff --git a/src/format/parse.rs b/src/format/parse.rs index 5132bda..c5b14a2 100644 --- a/src/format/parse.rs +++ b/src/format/parse.rs @@ -9,7 +9,7 @@ use std::usize; use Weekday; use super::scan; -use super::{Parsed, ParseResult, Item}; +use super::{Parsed, ParseResult, Item, InternalFixed, InternalInternal}; use super::{OUT_OF_RANGE, INVALID, TOO_SHORT, TOO_LONG, BAD_FORMAT}; fn set_weekday_with_num_days_from_sunday(p: &mut Parsed, v: i64) -> ParseResult<()> { @@ -328,12 +328,14 @@ pub fn parse<'a, I>(parsed: &mut Parsed, mut s: &str, items: I) -> ParseResult<( scan::colon_or_space)); try!(parsed.set_offset(i64::from(offset))); } + Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive }) => { + let offset = try_consume!(scan::timezone_offset_permissive( + s.trim_left(), scan::colon_or_space)); + try!(parsed.set_offset(i64::from(offset))); + } RFC2822 => try_consume!(parse_rfc2822(parsed, s)), RFC3339 => try_consume!(parse_rfc3339(parsed, s)), - - // for the future expansion - Internal(ref int) => match int._dummy {}, } } @@ -570,6 +572,10 @@ fn test_parse() { check!("zulu", [fix!(TimezoneOffsetZ), lit!("ulu")]; offset: 0); check!("+1234ulu", [fix!(TimezoneOffsetZ), lit!("ulu")]; offset: 754 * 60); check!("+12:34ulu", [fix!(TimezoneOffsetZ), lit!("ulu")]; offset: 754 * 60); + check!("Z", [internal_fix!(TimezoneOffsetPermissive)]; offset: 0); + check!("z", [internal_fix!(TimezoneOffsetPermissive)]; offset: 0); + check!("+12:00", [internal_fix!(TimezoneOffsetPermissive)]; offset: 12 * 60 * 60); + check!("+12", [internal_fix!(TimezoneOffsetPermissive)]; offset: 12 * 60 * 60); check!("???", [fix!(TimezoneName)]; BAD_FORMAT); // not allowed // some practical examples diff --git a/src/format/scan.rs b/src/format/scan.rs index 154369b..4a00641 100644 --- a/src/format/scan.rs +++ b/src/format/scan.rs @@ -171,8 +171,15 @@ pub fn colon_or_space(s: &str) -> ParseResult<&str> { /// /// The additional `colon` may be used to parse a mandatory or optional `:` /// between hours and minutes, and should return either a new suffix or `Err` when parsing fails. -pub fn timezone_offset(mut s: &str, mut colon: F) -> ParseResult<(&str, i32)> +pub fn timezone_offset(s: &str, consume_colon: F) -> ParseResult<(&str, i32)> where F: FnMut(&str) -> ParseResult<&str> { + timezone_offset_internal(s, consume_colon, false) +} + +fn timezone_offset_internal(mut s: &str, mut consume_colon: F, allow_missing_minutes: bool) +-> ParseResult<(&str, i32)> + where F: FnMut(&str) -> ParseResult<&str> +{ fn digits(s: &str) -> ParseResult<(u8, u8)> { let b = s.as_bytes(); if b.len() < 2 { @@ -197,29 +204,54 @@ pub fn timezone_offset(mut s: &str, mut colon: F) -> ParseResult<(&str, i32)> s = &s[2..]; // colons (and possibly other separators) - s = try!(colon(s)); + s = try!(consume_colon(s)); // minutes (00--59) - let minutes = match try!(digits(s)) { - (m1 @ b'0'...b'5', m2 @ b'0'...b'9') => i32::from((m1 - b'0') * 10 + (m2 - b'0')), - (b'6'...b'9', b'0'...b'9') => return Err(OUT_OF_RANGE), - _ => return Err(INVALID), + // if the next two items are digits then we have to add minutes + let minutes = if let Ok(ds) = digits(s) { + match ds { + (m1 @ b'0'...b'5', m2 @ b'0'...b'9') => i32::from((m1 - b'0') * 10 + (m2 - b'0')), + (b'6'...b'9', b'0'...b'9') => return Err(OUT_OF_RANGE), + _ => return Err(INVALID), + } + } else if allow_missing_minutes { + 0 + } else { + return Err(TOO_SHORT); + }; + s = match s.len() { + len if len >= 2 => &s[2..], + len if len == 0 => s, + _ => return Err(TOO_SHORT), }; - s = &s[2..]; let seconds = hours * 3600 + minutes * 60; Ok((s, if negative {-seconds} else {seconds})) } /// Same to `timezone_offset` but also allows for `z`/`Z` which is same to `+00:00`. -pub fn timezone_offset_zulu(s: &str, colon: F) -> ParseResult<(&str, i32)> - where F: FnMut(&str) -> ParseResult<&str> { +pub fn timezone_offset_zulu(s: &str, colon: F) +-> ParseResult<(&str, i32)> + where F: FnMut(&str) -> ParseResult<&str> +{ match s.as_bytes().first() { Some(&b'z') | Some(&b'Z') => Ok((&s[1..], 0)), _ => timezone_offset(s, colon), } } +/// Same to `timezone_offset` but also allows for `z`/`Z` which is same to +/// `+00:00`, and allows missing minutes entirely. +pub fn timezone_offset_permissive(s: &str, colon: F) +-> ParseResult<(&str, i32)> + where F: FnMut(&str) -> ParseResult<&str> +{ + match s.as_bytes().first() { + Some(&b'z') | Some(&b'Z') => Ok((&s[1..], 0)), + _ => timezone_offset_internal(s, colon, true), + } +} + /// Same to `timezone_offset` but also allows for RFC 2822 legacy timezones. /// May return `None` which indicates an insufficient offset data (i.e. `-0000`). pub fn timezone_offset_2822(s: &str) -> ParseResult<(&str, Option)> { diff --git a/src/format/strftime.rs b/src/format/strftime.rs index 86cf47b..3d65019 100644 --- a/src/format/strftime.rs +++ b/src/format/strftime.rs @@ -68,6 +68,7 @@ The following specifiers are available both to formatting and parsing. | `%Z` | `ACST` | *Formatting only:* Local time zone name. | | `%z` | `+0930` | Offset from the local time to UTC (with UTC being `+0000`). | | `%:z` | `+09:30` | Same to `%z` but with a colon. | +| `%#z` | `+09` | *Parsing only:* Same to `%z` but allows minutes to be missing or present. | | | | | | | | **DATE & TIME SPECIFIERS:** | |`%c`|`Sun Jul 8 00:34:60 2001`|`ctime` date & time format. Same to `%a %b %e %T %Y` sans `\n`.| @@ -146,7 +147,7 @@ Notes: */ -use super::{Item, Numeric, Fixed, Pad}; +use super::{Item, Numeric, Fixed, InternalFixed, InternalInternal, Pad}; /// Parsing iterator for `strftime`-like format strings. #[derive(Clone, Debug)] @@ -167,6 +168,8 @@ impl<'a> StrftimeItems<'a> { } } +const HAVE_ALTERNATES: &'static str = "z"; + impl<'a> Iterator for StrftimeItems<'a> { type Item = Item<'a>; @@ -205,7 +208,11 @@ impl<'a> Iterator for StrftimeItems<'a> { '_' => Some(Pad::Space), _ => None, }; - let spec = if pad_override.is_some() { next!() } else { spec }; + let is_alternate = spec == '#'; + let spec = if pad_override.is_some() || is_alternate { next!() } else { spec }; + if is_alternate && !HAVE_ALTERNATES.contains(spec) { + return Some(Item::Error); + } macro_rules! recons { [$head:expr, $($tail:expr),+] => ({ @@ -262,7 +269,11 @@ impl<'a> Iterator for StrftimeItems<'a> { 'x' => recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"), num0!(YearMod100)], 'y' => num0!(YearMod100), - 'z' => fix!(TimezoneOffset), + 'z' => if is_alternate { + internal_fix!(TimezoneOffsetPermissive) + } else { + fix!(TimezoneOffset) + }, '+' => fix!(RFC3339), ':' => match next!() { 'z' => fix!(TimezoneOffsetColon), @@ -368,6 +379,9 @@ fn test_strftime_items() { assert_eq!(parse_and_collect("%-e"), [num!(Day)]); assert_eq!(parse_and_collect("%0e"), [num0!(Day)]); assert_eq!(parse_and_collect("%_e"), [nums!(Day)]); + assert_eq!(parse_and_collect("%z"), [fix!(TimezoneOffset)]); + assert_eq!(parse_and_collect("%#z"), [internal_fix!(TimezoneOffsetPermissive)]); + assert_eq!(parse_and_collect("%#m"), [Item::Error]); } #[cfg(test)]