Merge pull request #242 from quodlibetor/add-permissive-tz-parsing

Add %#z as "Permissive" timezone parsing
This commit is contained in:
Brandon W Maister 2018-06-10 20:38:09 -04:00 committed by GitHub
commit 3c6e807672
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 84 additions and 40 deletions

View File

@ -213,29 +213,22 @@ pub enum Fixed {
} }
/// An opaque type representing fixed-format item types for internal uses only. /// An opaque type representing fixed-format item types for internal uses only.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InternalFixed { pub struct InternalFixed {
_dummy: Void, val: InternalInternal,
} }
impl Clone for InternalFixed { #[derive(Debug, Clone, PartialEq, Eq)]
fn clone(&self) -> Self { enum InternalInternal {
match self._dummy {} /// Same as [`TimezoneOffsetColonZ`](#variant.TimezoneOffsetColonZ), but
} /// allows missing minutes (per [ISO 8601][iso8601]).
} ///
/// # Panics
impl PartialEq for InternalFixed { ///
fn eq(&self, _other: &InternalFixed) -> bool { /// If you try to use this for printing.
match self._dummy {} ///
} /// [iso8601]: https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC
} TimezoneOffsetPermissive,
impl Eq for InternalFixed {
}
impl fmt::Debug for InternalFixed {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "<InternalFixed>")
}
} }
/// A single formatting item. This is used for both formatting and parsing. /// 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! num0 { ($x:ident) => (Item::Numeric(Numeric::$x, Pad::Zero)) }
macro_rules! nums { ($x:ident) => (Item::Numeric(Numeric::$x, Pad::Space)) } macro_rules! nums { ($x:ident) => (Item::Numeric(Numeric::$x, Pad::Space)) }
macro_rules! fix { ($x:ident) => (Item::Fixed(Fixed::$x)) } 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. /// An error from the `parse` function.
#[derive(Debug, Clone, PartialEq, Eq, Copy)] #[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)), off.map(|&(_, off)| write_local_minus_utc(w, off, false, false)),
TimezoneOffsetZ => TimezoneOffsetZ =>
off.map(|&(_, off)| write_local_minus_utc(w, off, true, false)), 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` RFC2822 => // same to `%a, %e %b %Y %H:%M:%S %z`
if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) { if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) {
let sec = t.second() + t.nanosecond() / 1_000_000_000; 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 { } else {
None None
}, },
// for the future expansion
Internal(ref int) => match int._dummy {},
}; };
match ret { match ret {
@ -609,4 +602,3 @@ impl FromStr for Weekday {
} }
} }
} }

View File

@ -9,7 +9,7 @@ use std::usize;
use Weekday; use Weekday;
use super::scan; 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}; 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<()> { 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)); scan::colon_or_space));
try!(parsed.set_offset(i64::from(offset))); 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)), RFC2822 => try_consume!(parse_rfc2822(parsed, s)),
RFC3339 => try_consume!(parse_rfc3339(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!("zulu", [fix!(TimezoneOffsetZ), lit!("ulu")]; offset: 0);
check!("+1234ulu", [fix!(TimezoneOffsetZ), lit!("ulu")]; offset: 754 * 60); check!("+1234ulu", [fix!(TimezoneOffsetZ), lit!("ulu")]; offset: 754 * 60);
check!("+12:34ulu", [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 check!("???", [fix!(TimezoneName)]; BAD_FORMAT); // not allowed
// some practical examples // some practical examples

View File

@ -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 `:` /// 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. /// between hours and minutes, and should return either a new suffix or `Err` when parsing fails.
pub fn timezone_offset<F>(mut s: &str, mut colon: F) -> ParseResult<(&str, i32)> pub fn timezone_offset<F>(s: &str, consume_colon: F) -> ParseResult<(&str, i32)>
where F: FnMut(&str) -> ParseResult<&str> { where F: FnMut(&str) -> ParseResult<&str> {
timezone_offset_internal(s, consume_colon, false)
}
fn timezone_offset_internal<F>(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)> { fn digits(s: &str) -> ParseResult<(u8, u8)> {
let b = s.as_bytes(); let b = s.as_bytes();
if b.len() < 2 { if b.len() < 2 {
@ -197,29 +204,54 @@ pub fn timezone_offset<F>(mut s: &str, mut colon: F) -> ParseResult<(&str, i32)>
s = &s[2..]; s = &s[2..];
// colons (and possibly other separators) // colons (and possibly other separators)
s = try!(colon(s)); s = try!(consume_colon(s));
// minutes (00--59) // minutes (00--59)
let minutes = match try!(digits(s)) { // if the next two items are digits then we have to add minutes
(m1 @ b'0'...b'5', m2 @ b'0'...b'9') => i32::from((m1 - b'0') * 10 + (m2 - b'0')), let minutes = if let Ok(ds) = digits(s) {
(b'6'...b'9', b'0'...b'9') => return Err(OUT_OF_RANGE), match ds {
_ => return Err(INVALID), (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; let seconds = hours * 3600 + minutes * 60;
Ok((s, if negative {-seconds} else {seconds})) Ok((s, if negative {-seconds} else {seconds}))
} }
/// Same to `timezone_offset` but also allows for `z`/`Z` which is same to `+00:00`. /// Same to `timezone_offset` but also allows for `z`/`Z` which is same to `+00:00`.
pub fn timezone_offset_zulu<F>(s: &str, colon: F) -> ParseResult<(&str, i32)> pub fn timezone_offset_zulu<F>(s: &str, colon: F)
where F: FnMut(&str) -> ParseResult<&str> { -> ParseResult<(&str, i32)>
where F: FnMut(&str) -> ParseResult<&str>
{
match s.as_bytes().first() { match s.as_bytes().first() {
Some(&b'z') | Some(&b'Z') => Ok((&s[1..], 0)), Some(&b'z') | Some(&b'Z') => Ok((&s[1..], 0)),
_ => timezone_offset(s, colon), _ => 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<F>(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. /// Same to `timezone_offset` but also allows for RFC 2822 legacy timezones.
/// May return `None` which indicates an insufficient offset data (i.e. `-0000`). /// May return `None` which indicates an insufficient offset data (i.e. `-0000`).
pub fn timezone_offset_2822(s: &str) -> ParseResult<(&str, Option<i32>)> { pub fn timezone_offset_2822(s: &str) -> ParseResult<(&str, Option<i32>)> {

View File

@ -68,6 +68,7 @@ The following specifiers are available both to formatting and parsing.
| `%Z` | `ACST` | *Formatting only:* Local time zone name. | | `%Z` | `ACST` | *Formatting only:* Local time zone name. |
| `%z` | `+0930` | Offset from the local time to UTC (with UTC being `+0000`). | | `%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: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:** | | | | **DATE & TIME SPECIFIERS:** |
|`%c`|`Sun Jul 8 00:34:60 2001`|`ctime` date & time format. Same to `%a %b %e %T %Y` sans `\n`.| |`%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. /// Parsing iterator for `strftime`-like format strings.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -167,6 +168,8 @@ impl<'a> StrftimeItems<'a> {
} }
} }
const HAVE_ALTERNATES: &'static str = "z";
impl<'a> Iterator for StrftimeItems<'a> { impl<'a> Iterator for StrftimeItems<'a> {
type Item = Item<'a>; type Item = Item<'a>;
@ -205,7 +208,11 @@ impl<'a> Iterator for StrftimeItems<'a> {
'_' => Some(Pad::Space), '_' => Some(Pad::Space),
_ => None, _ => 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 { macro_rules! recons {
[$head:expr, $($tail:expr),+] => ({ [$head:expr, $($tail:expr),+] => ({
@ -262,7 +269,11 @@ impl<'a> Iterator for StrftimeItems<'a> {
'x' => recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"), 'x' => recons![num0!(Month), lit!("/"), num0!(Day), lit!("/"),
num0!(YearMod100)], num0!(YearMod100)],
'y' => num0!(YearMod100), 'y' => num0!(YearMod100),
'z' => fix!(TimezoneOffset), 'z' => if is_alternate {
internal_fix!(TimezoneOffsetPermissive)
} else {
fix!(TimezoneOffset)
},
'+' => fix!(RFC3339), '+' => fix!(RFC3339),
':' => match next!() { ':' => match next!() {
'z' => fix!(TimezoneOffsetColon), 'z' => fix!(TimezoneOffsetColon),
@ -368,6 +379,9 @@ fn test_strftime_items() {
assert_eq!(parse_and_collect("%-e"), [num!(Day)]); assert_eq!(parse_and_collect("%-e"), [num!(Day)]);
assert_eq!(parse_and_collect("%0e"), [num0!(Day)]); assert_eq!(parse_and_collect("%0e"), [num0!(Day)]);
assert_eq!(parse_and_collect("%_e"), [nums!(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)] #[cfg(test)]