GNU coreutils date-like time zone formatting (#759)

Co-authored-by: Eric Sheppard <key2@eric.sheppard.cloud>
This commit is contained in:
Jarosław Konik 2022-08-17 18:41:48 +02:00 committed by GitHub
parent a383abf30e
commit 5f5410163b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 78 additions and 21 deletions

View File

@ -49,6 +49,7 @@ Versions with only mechanical changes will be omitted from the following list.
* Add compatibility with rfc2822 comments (#733) * 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 * 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 the `Months` struct and associated `Add` and `Sub` impls
* Add `GNU` `coreutils` `date`-like time zone formatting
## 0.4.19 ## 0.4.19
@ -774,4 +775,3 @@ and replaced by 0.2.25 very shortly. Duh.)
## 0.1.0 (2014-11-20) ## 0.1.0 (2014-11-20)
The initial version that was available to `crates.io`. The initial version that was available to `crates.io`.

View File

@ -224,6 +224,18 @@ pub enum Fixed {
/// The offset is limited from `-24:00` to `+24:00`, /// The offset is limited from `-24:00` to `+24:00`,
/// which is the same as [`FixedOffset`](../offset/struct.FixedOffset.html)'s range. /// which is the same as [`FixedOffset`](../offset/struct.FixedOffset.html)'s range.
TimezoneOffsetColon, 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`). /// 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, /// In the parser, the colon can be omitted and/or surrounded with any amount of whitespace,
@ -274,6 +286,15 @@ enum InternalInternal {
Nanosecond9NoDot, 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. /// A single formatting item. This is used for both formatting and parsing.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum Item<'a> { pub enum Item<'a> {
@ -557,15 +578,32 @@ fn format_inner<'a>(
result: &mut String, result: &mut String,
off: FixedOffset, off: FixedOffset,
allow_zulu: bool, allow_zulu: bool,
use_colon: bool, colon_type: Colons,
) -> fmt::Result { ) -> fmt::Result {
let off = off.local_minus_utc(); let off = off.local_minus_utc();
if !allow_zulu || off != 0 { if !allow_zulu || off != 0 {
let (sign, off) = if off < 0 { ('-', -off) } else { ('+', off) }; let (sign, off) = if off < 0 { ('-', -off) } else { ('+', off) };
if use_colon {
write!(result, "{}{:02}:{:02}", sign, off / 3600, off / 60 % 60) match colon_type {
} else { Colons::None => {
write!(result, "{}{:02}{:02}", sign, off / 3600, off / 60 % 60) 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 { } else {
result.push('Z'); result.push('Z');
@ -650,17 +688,19 @@ fn format_inner<'a>(
result.push_str(name); result.push_str(name);
Ok(()) Ok(())
}), }),
TimezoneOffsetColon => { TimezoneOffsetColon => off
off.map(|&(_, off)| write_local_minus_utc(result, off, false, true)) .map(|&(_, off)| write_local_minus_utc(result, off, false, Colons::Single)),
} TimezoneOffsetDoubleColon => off
TimezoneOffsetColonZ => { .map(|&(_, off)| write_local_minus_utc(result, off, false, Colons::Double)),
off.map(|&(_, off)| write_local_minus_utc(result, off, true, true)) 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 => { 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 => { 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 }) => { Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive }) => {
panic!("Do not try to write %#z it is undefined") panic!("Do not try to write %#z it is undefined")
@ -681,7 +721,7 @@ fn format_inner<'a>(
t.minute(), t.minute(),
sec sec
)?; )?;
Some(write_local_minus_utc(result, off, false, false)) Some(write_local_minus_utc(result, off, false, Colons::None))
} else { } else {
None None
} }
@ -693,7 +733,7 @@ fn format_inner<'a>(
// reuse `Debug` impls which already print ISO 8601 format. // reuse `Debug` impls which already print ISO 8601 format.
// this is faster in this way. // this is faster in this way.
write!(result, "{:?}T{:?}", d, t)?; 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 { } else {
None None
} }

View File

@ -420,7 +420,10 @@ where
try_consume!(scan::timezone_name_skip(s)); try_consume!(scan::timezone_name_skip(s));
} }
&TimezoneOffsetColon | &TimezoneOffset => { &TimezoneOffsetColon
| &TimezoneOffsetDoubleColon
| &TimezoneOffsetTripleColon
| &TimezoneOffset => {
let offset = try_consume!(scan::timezone_offset( let offset = try_consume!(scan::timezone_offset(
s.trim_left(), s.trim_left(),
scan::colon_or_space scan::colon_or_space

View File

@ -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` | `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` | `+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` | 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. | | `%#z` | `+09` | *Parsing only:* Same as `%z` but allows minutes to be missing or present. |
| | | | | | | |
| | | **DATE & TIME SPECIFIERS:** | | | | **DATE & TIME SPECIFIERS:** |
@ -404,10 +406,20 @@ impl<'a> Iterator for StrftimeItems<'a> {
} }
} }
'+' => fix!(RFC3339), '+' => fix!(RFC3339),
':' => match next!() { ':' => {
'z' => fix!(TimezoneOffsetColon), if self.remainder.starts_with("::z") {
_ => Item::Error, 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!() { '.' => match next!() {
'3' => match next!() { '3' => match next!() {
'f' => fix!(Nanosecond3), '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(), "ACST");
assert_eq!(dt.format("%z").to_string(), "+0930"); 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");
assert_eq!(dt.format("%::z").to_string(), "+09:30:00");
assert_eq!(dt.format("%:::z").to_string(), "+09");
// date & time specifiers // date & time specifiers
assert_eq!(dt.format("%c").to_string(), "Sun Jul 8 00:34:60 2001"); assert_eq!(dt.format("%c").to_string(), "Sun Jul 8 00:34:60 2001");