From 12ef137bf598eee1c6c12831a90149c43670cb03 Mon Sep 17 00:00:00 2001 From: Jaroslaw Konik Date: Sat, 6 Aug 2022 13:57:09 +0200 Subject: [PATCH 1/9] Implement "GNU coreutils date"-like time zone formatting --- src/format/mod.rs | 80 +++++++++++++++++++++++++++++++++--------- src/format/parse.rs | 5 ++- src/format/strftime.rs | 35 +++++++++++++++--- 3 files changed, 99 insertions(+), 21 deletions(-) diff --git a/src/format/mod.rs b/src/format/mod.rs index 2089c4c06a..53c058d56c 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -224,7 +224,19 @@ 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 (`+09:00` or `-04:00` or `Z`). + /// Offset from the local time to UTC (`+09:00` or `-04:00` or `+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` to `+24:00`, + /// which is the same as [`FixedOffset`](../offset/struct.FixedOffset.html)'s range. + TimezoneOffsetDoubleColon, + /// 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` to `+24:00`, + /// which is the same as [`FixedOffset`](../offset/struct.FixedOffset.html)'s range. + TimezoneOffsetTripleColon, + /// 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, /// and `Z` can be either in upper case or in lower case. @@ -274,6 +286,14 @@ enum InternalInternal { Nanosecond9NoDot, } +#[derive(Debug, Clone, PartialEq, Eq)] +enum ColonType { + NoColon, + SingleColon, + DoubleColon, + TripleColon, +} + /// A single formatting item. This is used for both formatting and parsing. #[derive(Clone, PartialEq, Eq, Debug)] pub enum Item<'a> { @@ -557,15 +577,32 @@ fn format_inner<'a>( result: &mut String, off: FixedOffset, allow_zulu: bool, - use_colon: bool, + colon_type: Option, ) -> 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.unwrap_or(ColonType::NoColon) { + ColonType::NoColon => { + write!(result, "{}{:02}{:02}", sign, off / 3600, off / 60 % 60) + } + ColonType::SingleColon => { + write!(result, "{}{:02}:{:02}", sign, off / 3600, off / 60 % 60) + } + ColonType::DoubleColon => { + write!( + result, + "{}{:02}:{:02}:{:02}", + sign, + off / 3600, + off / 60 % 60, + off % 60 + ) + } + ColonType::TripleColon => { + write!(result, "{}{:02}", sign, off / 3600) + } } } else { result.push('Z'); @@ -650,17 +687,23 @@ 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, Some(ColonType::SingleColon)) + }), + TimezoneOffsetDoubleColon => off.map(|&(_, off)| { + write_local_minus_utc(result, off, false, Some(ColonType::DoubleColon)) + }), + TimezoneOffsetTripleColon => off.map(|&(_, off)| { + write_local_minus_utc(result, off, false, Some(ColonType::TripleColon)) + }), + TimezoneOffsetColonZ => off.map(|&(_, off)| { + write_local_minus_utc(result, off, true, Some(ColonType::SingleColon)) + }), TimezoneOffset => { - off.map(|&(_, off)| write_local_minus_utc(result, off, false, false)) + off.map(|&(_, off)| write_local_minus_utc(result, off, false, None)) } TimezoneOffsetZ => { - off.map(|&(_, off)| write_local_minus_utc(result, off, true, false)) + off.map(|&(_, off)| write_local_minus_utc(result, off, true, None)) } Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive }) => { panic!("Do not try to write %#z it is undefined") @@ -681,7 +724,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, None)) } else { None } @@ -693,7 +736,12 @@ 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, + Some(ColonType::SingleColon), + )) } else { None } diff --git a/src/format/parse.rs b/src/format/parse.rs index 0e2db8fae9..02771b2e42 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 58169d125f..e211c41d40 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. [^9] | | `%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:** | @@ -400,10 +402,33 @@ impl<'a> Iterator for StrftimeItems<'a> { } } '+' => fix!(RFC3339), - ':' => match next!() { - 'z' => fix!(TimezoneOffsetColon), - _ => Item::Error, - }, + ':' => { + let mut num_colon = 1; + + let result = loop { + match next!() { + ':' => { + num_colon += 1; + } + 'z' => { + break Ok(()); + } + _ => { + break Err(Item::Error); + } + } + }; + + match result { + Ok(_) => match num_colon { + 1 => fix!(TimezoneOffsetColon), + 2 => fix!(TimezoneOffsetDoubleColon), + 3 => fix!(TimezoneOffsetTripleColon), + _ => Item::Error, + }, + Err(e) => e, + } + } '.' => match next!() { '3' => match next!() { 'f' => fix!(Nanosecond3), @@ -592,6 +617,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"); From 4e068bfa117fd7d1ca86121a7251ffe121dfc2db Mon Sep 17 00:00:00 2001 From: Jaroslaw Konik Date: Sat, 6 Aug 2022 14:34:55 +0200 Subject: [PATCH 2/9] add change to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be98bd3e33..85b4283c87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,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 @@ -762,4 +763,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`. - From c57fd3f4b7cdd4ec92dfe0a1c0dca1081ba35ca4 Mon Sep 17 00:00:00 2001 From: Jaroslaw Konik Date: Sat, 6 Aug 2022 14:37:43 +0200 Subject: [PATCH 3/9] fix --- src/format/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/format/mod.rs b/src/format/mod.rs index 53c058d56c..cfe9e91ba4 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -236,7 +236,7 @@ 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. TimezoneOffsetTripleColon, - /// Offset from the local time to UTC without minutes (`+09` or `-04` or `+00`). + /// 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, /// and `Z` can be either in upper case or in lower case. From 2009135465817cfbba61ddffa224f519241ba9be Mon Sep 17 00:00:00 2001 From: Jaroslaw Konik Date: Sat, 6 Aug 2022 14:38:40 +0200 Subject: [PATCH 4/9] fix docs --- src/format/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/format/mod.rs b/src/format/mod.rs index cfe9e91ba4..19f1cfb1d7 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -224,13 +224,13 @@ 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 (`+09:00` or `-04:00` or `+00:00`). + /// 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` to `+24:00`, /// which is the same as [`FixedOffset`](../offset/struct.FixedOffset.html)'s range. TimezoneOffsetDoubleColon, - /// Offset from the local time to UTC with seconds (`+09:00:00` or `-04:00:00` or `+00:00:00`). + /// 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:00` to `+24:00`, From e0c74d4a3e4283f755f0d876b0fcec7b0b95d467 Mon Sep 17 00:00:00 2001 From: Jaroslaw Konik Date: Sat, 6 Aug 2022 14:44:12 +0200 Subject: [PATCH 5/9] fix clippy error --- src/format/mod.rs | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/format/mod.rs b/src/format/mod.rs index 19f1cfb1d7..e1613e5cc7 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -288,10 +288,10 @@ enum InternalInternal { #[derive(Debug, Clone, PartialEq, Eq)] enum ColonType { - NoColon, - SingleColon, - DoubleColon, - TripleColon, + None, + Single, + Double, + Triple, } /// A single formatting item. This is used for both formatting and parsing. @@ -583,14 +583,14 @@ fn format_inner<'a>( if !allow_zulu || off != 0 { let (sign, off) = if off < 0 { ('-', -off) } else { ('+', off) }; - match colon_type.unwrap_or(ColonType::NoColon) { - ColonType::NoColon => { + match colon_type.unwrap_or(ColonType::None) { + ColonType::None => { write!(result, "{}{:02}{:02}", sign, off / 3600, off / 60 % 60) } - ColonType::SingleColon => { + ColonType::Single => { write!(result, "{}{:02}:{:02}", sign, off / 3600, off / 60 % 60) } - ColonType::DoubleColon => { + ColonType::Double => { write!( result, "{}{:02}:{:02}:{:02}", @@ -600,7 +600,7 @@ fn format_inner<'a>( off % 60 ) } - ColonType::TripleColon => { + ColonType::Triple => { write!(result, "{}{:02}", sign, off / 3600) } } @@ -688,16 +688,16 @@ fn format_inner<'a>( Ok(()) }), TimezoneOffsetColon => off.map(|&(_, off)| { - write_local_minus_utc(result, off, false, Some(ColonType::SingleColon)) + write_local_minus_utc(result, off, false, Some(ColonType::Single)) }), TimezoneOffsetDoubleColon => off.map(|&(_, off)| { - write_local_minus_utc(result, off, false, Some(ColonType::DoubleColon)) + write_local_minus_utc(result, off, false, Some(ColonType::Double)) }), TimezoneOffsetTripleColon => off.map(|&(_, off)| { - write_local_minus_utc(result, off, false, Some(ColonType::TripleColon)) + write_local_minus_utc(result, off, false, Some(ColonType::Triple)) }), TimezoneOffsetColonZ => off.map(|&(_, off)| { - write_local_minus_utc(result, off, true, Some(ColonType::SingleColon)) + write_local_minus_utc(result, off, true, Some(ColonType::Single)) }), TimezoneOffset => { off.map(|&(_, off)| write_local_minus_utc(result, off, false, None)) @@ -736,12 +736,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, - Some(ColonType::SingleColon), - )) + Some(write_local_minus_utc(result, off, false, Some(ColonType::Single))) } else { None } From 7abaa7aa048870110fa1ea821431df0e4950ad2b Mon Sep 17 00:00:00 2001 From: Jaroslaw Konik Date: Sat, 6 Aug 2022 14:59:22 +0200 Subject: [PATCH 6/9] add feature annotation --- src/format/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/format/mod.rs b/src/format/mod.rs index e1613e5cc7..4d2e2efc66 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -286,6 +286,7 @@ enum InternalInternal { Nanosecond9NoDot, } +#[cfg(any(feature = "alloc", feature = "std", test))] #[derive(Debug, Clone, PartialEq, Eq)] enum ColonType { None, From d30d492205b1914923573477b01dc74f7216a172 Mon Sep 17 00:00:00 2001 From: Jaroslaw Konik Date: Tue, 16 Aug 2022 19:58:07 +0200 Subject: [PATCH 7/9] Rename enum, dont use Option --- src/format/mod.rs | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/format/mod.rs b/src/format/mod.rs index 4d2e2efc66..ad7351602e 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -288,7 +288,7 @@ enum InternalInternal { #[cfg(any(feature = "alloc", feature = "std", test))] #[derive(Debug, Clone, PartialEq, Eq)] -enum ColonType { +enum Colons { None, Single, Double, @@ -578,20 +578,20 @@ fn format_inner<'a>( result: &mut String, off: FixedOffset, allow_zulu: bool, - colon_type: Option, + 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) }; - match colon_type.unwrap_or(ColonType::None) { - ColonType::None => { + match colon_type { + Colons::None => { write!(result, "{}{:02}{:02}", sign, off / 3600, off / 60 % 60) } - ColonType::Single => { + Colons::Single => { write!(result, "{}{:02}:{:02}", sign, off / 3600, off / 60 % 60) } - ColonType::Double => { + Colons::Double => { write!( result, "{}{:02}:{:02}:{:02}", @@ -601,7 +601,7 @@ fn format_inner<'a>( off % 60 ) } - ColonType::Triple => { + Colons::Triple => { write!(result, "{}{:02}", sign, off / 3600) } } @@ -688,23 +688,19 @@ fn format_inner<'a>( result.push_str(name); Ok(()) }), - TimezoneOffsetColon => off.map(|&(_, off)| { - write_local_minus_utc(result, off, false, Some(ColonType::Single)) - }), - TimezoneOffsetDoubleColon => off.map(|&(_, off)| { - write_local_minus_utc(result, off, false, Some(ColonType::Double)) - }), - TimezoneOffsetTripleColon => off.map(|&(_, off)| { - write_local_minus_utc(result, off, false, Some(ColonType::Triple)) - }), - TimezoneOffsetColonZ => off.map(|&(_, off)| { - write_local_minus_utc(result, off, true, Some(ColonType::Single)) - }), + 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, None)) + off.map(|&(_, off)| write_local_minus_utc(result, off, false, Colons::None)) } TimezoneOffsetZ => { - off.map(|&(_, off)| write_local_minus_utc(result, off, true, None)) + 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") @@ -725,7 +721,7 @@ fn format_inner<'a>( t.minute(), sec )?; - Some(write_local_minus_utc(result, off, false, None)) + Some(write_local_minus_utc(result, off, false, Colons::None)) } else { None } @@ -737,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, Some(ColonType::Single))) + Some(write_local_minus_utc(result, off, false, Colons::Single)) } else { None } From eab88d43e0bbae28907bace816b912aad3ccc9c4 Mon Sep 17 00:00:00 2001 From: Eric Sheppard Date: Wed, 17 Aug 2022 22:13:34 +1000 Subject: [PATCH 8/9] remove loop remove match option --- src/format/strftime.rs | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/format/strftime.rs b/src/format/strftime.rs index e211c41d40..5634dda38e 100644 --- a/src/format/strftime.rs +++ b/src/format/strftime.rs @@ -403,30 +403,17 @@ impl<'a> Iterator for StrftimeItems<'a> { } '+' => fix!(RFC3339), ':' => { - let mut num_colon = 1; - - let result = loop { - match next!() { - ':' => { - num_colon += 1; - } - 'z' => { - break Ok(()); - } - _ => { - break Err(Item::Error); - } - } - }; - - match result { - Ok(_) => match num_colon { - 1 => fix!(TimezoneOffsetColon), - 2 => fix!(TimezoneOffsetDoubleColon), - 3 => fix!(TimezoneOffsetTripleColon), - _ => Item::Error, - }, - Err(e) => e, + 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!() { From 3a2dc4ad079183e308ec864b1364a0202483a93f Mon Sep 17 00:00:00 2001 From: Jaroslaw Konik Date: Wed, 17 Aug 2022 17:54:12 +0200 Subject: [PATCH 9/9] Adjust docs --- src/format/mod.rs | 4 ++-- src/format/strftime.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/format/mod.rs b/src/format/mod.rs index ad7351602e..fef5547439 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -227,13 +227,13 @@ pub enum Fixed { /// 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` to `+24:00`, + /// 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:00` to `+24:00`, + /// 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`). diff --git a/src/format/strftime.rs b/src/format/strftime.rs index 5634dda38e..6f15538f91 100644 --- a/src/format/strftime.rs +++ b/src/format/strftime.rs @@ -71,8 +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. [^9] | | `%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: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:** |