Add %#z as "Permissive" timezone parsing
This allows you to parse a timezone that: * Is either `Z` or an actual offset * Contains no minutes, just the hour Fixes #219
This commit is contained in:
parent
9276929c58
commit
95f6a2be1c
|
@ -200,6 +200,15 @@ pub enum Fixed {
|
||||||
/// Same to [`TimezoneOffsetColonZ`](#variant.TimezoneOffsetColonZ) but prints no colon.
|
/// Same to [`TimezoneOffsetColonZ`](#variant.TimezoneOffsetColonZ) but prints no colon.
|
||||||
/// Parsing allows an optional colon.
|
/// Parsing allows an optional colon.
|
||||||
TimezoneOffsetZ,
|
TimezoneOffsetZ,
|
||||||
|
/// 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,
|
||||||
/// RFC 2822 date and time syntax. Commonly used for email and MIME date and time.
|
/// RFC 2822 date and time syntax. Commonly used for email and MIME date and time.
|
||||||
RFC2822,
|
RFC2822,
|
||||||
/// RFC 3339 & ISO 8601 date and time syntax.
|
/// RFC 3339 & ISO 8601 date and time syntax.
|
||||||
|
@ -491,6 +500,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)),
|
||||||
|
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;
|
||||||
|
@ -609,4 +620,3 @@ impl FromStr for Weekday {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -328,6 +328,11 @@ 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)));
|
||||||
}
|
}
|
||||||
|
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)),
|
||||||
|
@ -570,6 +575,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", [fix!(TimezoneOffsetPermissive)]; offset: 0);
|
||||||
|
check!("z", [fix!(TimezoneOffsetPermissive)]; offset: 0);
|
||||||
|
check!("+12:00", [fix!(TimezoneOffsetPermissive)]; offset: 12 * 60 * 60);
|
||||||
|
check!("+12", [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
|
||||||
|
|
|
@ -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
|
||||||
|
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')),
|
(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),
|
(b'6'...b'9', b'0'...b'9') => return Err(OUT_OF_RANGE),
|
||||||
_ => return Err(INVALID),
|
_ => 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>)> {
|
||||||
|
|
|
@ -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`.|
|
||||||
|
@ -167,6 +168,8 @@ impl<'a> StrftimeItems<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HAVE_ALTERNATES: &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 {
|
||||||
|
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"), [fix!(TimezoneOffsetPermissive)]);
|
||||||
|
assert_eq!(parse_and_collect("%#m"), [Item::Error]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
Loading…
Reference in New Issue