From 5f5410163b6655cd0098530f2c6c44bbcc87d102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Konik?= Date: Wed, 17 Aug 2022 18:41:48 +0200 Subject: [PATCH] GNU coreutils `date`-like time zone formatting (#759) Co-authored-by: Eric Sheppard --- CHANGELOG.md | 2 +- src/format/mod.rs | 70 +++++++++++++++++++++++++++++++++--------- src/format/parse.rs | 5 ++- src/format/strftime.rs | 22 ++++++++++--- 4 files changed, 78 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9fd981e..a60d88c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Versions with only mechanical changes will be omitted from the following list. * Add compatibility with rfc2822 comments (#733) * Make `js-sys` and `wasm-bindgen` enabled by default when target is `wasm32-unknown-unknown` for ease of API discovery * Add the `Months` struct and associated `Add` and `Sub` impls +* Add `GNU` `coreutils` `date`-like time zone formatting ## 0.4.19 @@ -774,4 +775,3 @@ and replaced by 0.2.25 very shortly. Duh.) ## 0.1.0 (2014-11-20) The initial version that was available to `crates.io`. - diff --git a/src/format/mod.rs b/src/format/mod.rs index 695ede79..b88ae4b6 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -224,6 +224,18 @@ pub enum Fixed { /// The offset is limited from `-24:00` to `+24:00`, /// which is the same as [`FixedOffset`](../offset/struct.FixedOffset.html)'s range. TimezoneOffsetColon, + /// Offset from the local time to UTC with seconds (`+09:00:00` or `-04:00:00` or `+00:00:00`). + /// + /// In the parser, the colon can be omitted and/or surrounded with any amount of whitespace. + /// The offset is limited from `-24:00:00` to `+24:00:00`, + /// which is the same as [`FixedOffset`](../offset/struct.FixedOffset.html)'s range. + TimezoneOffsetDoubleColon, + /// Offset from the local time to UTC without minutes (`+09` or `-04` or `+00`). + /// + /// In the parser, the colon can be omitted and/or surrounded with any amount of whitespace. + /// The offset is limited from `-24` to `+24`, + /// which is the same as [`FixedOffset`](../offset/struct.FixedOffset.html)'s range. + TimezoneOffsetTripleColon, /// Offset from the local time to UTC (`+09:00` or `-04:00` or `Z`). /// /// In the parser, the colon can be omitted and/or surrounded with any amount of whitespace, @@ -274,6 +286,15 @@ enum InternalInternal { Nanosecond9NoDot, } +#[cfg(any(feature = "alloc", feature = "std", test))] +#[derive(Debug, Clone, PartialEq, Eq)] +enum Colons { + None, + Single, + Double, + Triple, +} + /// A single formatting item. This is used for both formatting and parsing. #[derive(Clone, PartialEq, Eq, Debug)] pub enum Item<'a> { @@ -557,15 +578,32 @@ fn format_inner<'a>( result: &mut String, off: FixedOffset, allow_zulu: bool, - use_colon: bool, + colon_type: Colons, ) -> fmt::Result { let off = off.local_minus_utc(); if !allow_zulu || off != 0 { let (sign, off) = if off < 0 { ('-', -off) } else { ('+', off) }; - if use_colon { - write!(result, "{}{:02}:{:02}", sign, off / 3600, off / 60 % 60) - } else { - write!(result, "{}{:02}{:02}", sign, off / 3600, off / 60 % 60) + + match colon_type { + Colons::None => { + write!(result, "{}{:02}{:02}", sign, off / 3600, off / 60 % 60) + } + Colons::Single => { + write!(result, "{}{:02}:{:02}", sign, off / 3600, off / 60 % 60) + } + Colons::Double => { + write!( + result, + "{}{:02}:{:02}:{:02}", + sign, + off / 3600, + off / 60 % 60, + off % 60 + ) + } + Colons::Triple => { + write!(result, "{}{:02}", sign, off / 3600) + } } } else { result.push('Z'); @@ -650,17 +688,19 @@ fn format_inner<'a>( result.push_str(name); Ok(()) }), - TimezoneOffsetColon => { - off.map(|&(_, off)| write_local_minus_utc(result, off, false, true)) - } - TimezoneOffsetColonZ => { - off.map(|&(_, off)| write_local_minus_utc(result, off, true, true)) - } + TimezoneOffsetColon => off + .map(|&(_, off)| write_local_minus_utc(result, off, false, Colons::Single)), + TimezoneOffsetDoubleColon => off + .map(|&(_, off)| write_local_minus_utc(result, off, false, Colons::Double)), + TimezoneOffsetTripleColon => off + .map(|&(_, off)| write_local_minus_utc(result, off, false, Colons::Triple)), + TimezoneOffsetColonZ => off + .map(|&(_, off)| write_local_minus_utc(result, off, true, Colons::Single)), TimezoneOffset => { - off.map(|&(_, off)| write_local_minus_utc(result, off, false, false)) + off.map(|&(_, off)| write_local_minus_utc(result, off, false, Colons::None)) } TimezoneOffsetZ => { - off.map(|&(_, off)| write_local_minus_utc(result, off, true, false)) + off.map(|&(_, off)| write_local_minus_utc(result, off, true, Colons::None)) } Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive }) => { panic!("Do not try to write %#z it is undefined") @@ -681,7 +721,7 @@ fn format_inner<'a>( t.minute(), sec )?; - Some(write_local_minus_utc(result, off, false, false)) + Some(write_local_minus_utc(result, off, false, Colons::None)) } else { None } @@ -693,7 +733,7 @@ fn format_inner<'a>( // reuse `Debug` impls which already print ISO 8601 format. // this is faster in this way. write!(result, "{:?}T{:?}", d, t)?; - Some(write_local_minus_utc(result, off, false, true)) + Some(write_local_minus_utc(result, off, false, Colons::Single)) } else { None } diff --git a/src/format/parse.rs b/src/format/parse.rs index 0e2db8fa..02771b2e 100644 --- a/src/format/parse.rs +++ b/src/format/parse.rs @@ -420,7 +420,10 @@ where try_consume!(scan::timezone_name_skip(s)); } - &TimezoneOffsetColon | &TimezoneOffset => { + &TimezoneOffsetColon + | &TimezoneOffsetDoubleColon + | &TimezoneOffsetTripleColon + | &TimezoneOffset => { let offset = try_consume!(scan::timezone_offset( s.trim_left(), scan::colon_or_space diff --git a/src/format/strftime.rs b/src/format/strftime.rs index b5504cfc..19e401ad 100644 --- a/src/format/strftime.rs +++ b/src/format/strftime.rs @@ -71,6 +71,8 @@ The following specifiers are available both to formatting and parsing. | `%Z` | `ACST` | Local time zone name. Skips all non-whitespace characters during parsing. [^8] | | `%z` | `+0930` | Offset from the local time to UTC (with UTC being `+0000`). | | `%:z` | `+09:30` | Same as `%z` but with a colon. | +|`%::z`|`+09:30:00`| Offset from the local time to UTC with seconds. | +|`%:::z`| `+09` | Offset from the local time to UTC without minutes. | | `%#z` | `+09` | *Parsing only:* Same as `%z` but allows minutes to be missing or present. | | | | | | | | **DATE & TIME SPECIFIERS:** | @@ -404,10 +406,20 @@ impl<'a> Iterator for StrftimeItems<'a> { } } '+' => fix!(RFC3339), - ':' => match next!() { - 'z' => fix!(TimezoneOffsetColon), - _ => Item::Error, - }, + ':' => { + if self.remainder.starts_with("::z") { + self.remainder = &self.remainder[3..]; + fix!(TimezoneOffsetTripleColon) + } else if self.remainder.starts_with(":z") { + self.remainder = &self.remainder[2..]; + fix!(TimezoneOffsetDoubleColon) + } else if self.remainder.starts_with('z') { + self.remainder = &self.remainder[1..]; + fix!(TimezoneOffsetColon) + } else { + Item::Error + } + } '.' => match next!() { '3' => match next!() { 'f' => fix!(Nanosecond3), @@ -596,6 +608,8 @@ fn test_strftime_docs() { //assert_eq!(dt.format("%Z").to_string(), "ACST"); assert_eq!(dt.format("%z").to_string(), "+0930"); assert_eq!(dt.format("%:z").to_string(), "+09:30"); + assert_eq!(dt.format("%::z").to_string(), "+09:30:00"); + assert_eq!(dt.format("%:::z").to_string(), "+09"); // date & time specifiers assert_eq!(dt.format("%c").to_string(), "Sun Jul 8 00:34:60 2001");