From 60c0f59337e967b719a8b53ef8663c42478723e0 Mon Sep 17 00:00:00 2001 From: Paul Dicker Date: Mon, 24 Apr 2023 15:22:48 +0200 Subject: [PATCH] Encode no offset info --- src/datetime/mod.rs | 12 ++++------ src/format/parse.rs | 39 +++++++++++++++++++++++--------- src/format/parsed.rs | 9 +++++++- src/format/scan.rs | 50 +++++++++++++++++++++------------------- src/offset/fixed.rs | 54 +++++++++++++++++++++++++++++++++++--------- src/offset/utc.rs | 2 +- 6 files changed, 112 insertions(+), 54 deletions(-) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index dc9349472a..c324d510f0 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -468,9 +468,9 @@ impl From> for DateTime { /// Convert this `DateTime` instance into a `DateTime` instance. /// /// Conversion is done via [`DateTime::with_timezone`]. Note that the converted value returned by - /// this will be created with a fixed timezone offset of 0. + /// this will be created with a fixed timezone offset `FixedOffset::NO_OFFSET`. fn from(src: DateTime) -> Self { - src.with_timezone(&FixedOffset::east_opt(0).unwrap()) + src.with_timezone(&FixedOffset::NO_OFFSET) } } @@ -530,9 +530,9 @@ impl From> for DateTime { /// Convert this `DateTime` instance into a `DateTime` instance. /// /// Conversion is performed via [`DateTime::with_timezone`]. Note that the converted value returned - /// by this will be created with a fixed timezone offset of 0. + /// by this will be created with a fixed timezone offset of `FixedOffset::NO_OFFSET`. fn from(src: DateTime) -> Self { - src.with_timezone(&FixedOffset::east_opt(0).unwrap()) + src.with_timezone(&FixedOffset::NO_OFFSET) } } @@ -1277,9 +1277,7 @@ fn test_decodable_json( assert_eq!( norm(&fixed_from_str(r#""2014-07-24T12:34:06Z""#).ok()), - norm(&Some( - FixedOffset::east_opt(0).unwrap().with_ymd_and_hms(2014, 7, 24, 12, 34, 6).unwrap() - )) + norm(&Some(FixedOffset::UTC.with_ymd_and_hms(2014, 7, 24, 12, 34, 6).unwrap())) ); assert_eq!( norm(&fixed_from_str(r#""2014-07-24T13:57:06+01:23""#).ok()), diff --git a/src/format/parse.rs b/src/format/parse.rs index e894f710b9..0316086bf7 100644 --- a/src/format/parse.rs +++ b/src/format/parse.rs @@ -15,6 +15,7 @@ use super::{Fixed, InternalFixed, InternalInternal, Item, Numeric, Pad, Parsed}; use super::{ParseError, ParseErrorKind, ParseResult}; use super::{BAD_FORMAT, INVALID, NOT_ENOUGH, OUT_OF_RANGE, TOO_LONG, TOO_SHORT}; use crate::{DateTime, FixedOffset, Weekday}; +use crate::format::parsed::NO_OFFSET_INFO; fn set_weekday_with_num_days_from_sunday(p: &mut Parsed, v: i64) -> ParseResult<()> { p.set_weekday(match v { @@ -144,9 +145,10 @@ fn parse_rfc2822<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st } s = scan::space(s)?; // mandatory - if let Some(offset) = try_consume!(scan::timezone_offset_2822(s)) { + match try_consume!(scan::timezone_offset_2822(s)) { // only set the offset when it is definitely known (i.e. not `-0000`) - parsed.set_offset(i64::from(offset))?; + Some(offset) => parsed.set_offset(i64::from(offset))?, + None => parsed.set_offset(NO_OFFSET_INFO)?, } // optional comments @@ -215,11 +217,14 @@ fn parse_rfc3339<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st parsed.set_nanosecond(nanosecond)?; } - let offset = try_consume!(scan::timezone_offset_zulu(s, |s| scan::char(s, b':'))); - if offset <= -86_400 || offset >= 86_400 { - return Err(OUT_OF_RANGE); + if let Some(offset) = try_consume!(scan::timezone_offset_zulu(s, |s| scan::char(s, b':'))) { + if offset <= -86_400 || offset >= 86_400 { + return Err(OUT_OF_RANGE); + } + parsed.set_offset(i64::from(offset))?; + } else { + parsed.set_offset(NO_OFFSET_INFO)?; } - parsed.set_offset(i64::from(offset))?; Ok((s, ())) } @@ -428,7 +433,11 @@ where s.trim_left(), scan::colon_or_space )); - parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?; + if let Some(offset) = offset { + parsed.set_offset(i64::from(offset)) + } else { + parsed.set_offset(NO_OFFSET_INFO) + }.map_err(|e| (s, e))?; } &TimezoneOffsetColonZ | &TimezoneOffsetZ => { @@ -436,7 +445,11 @@ where s.trim_left(), scan::colon_or_space )); - parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?; + if let Some(offset) = offset { + parsed.set_offset(i64::from(offset)) + } else { + parsed.set_offset(NO_OFFSET_INFO) + }.map_err(|e| (s, e))?; } &Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive, @@ -445,7 +458,11 @@ where s.trim_left(), scan::colon_or_space )); - parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?; + if let Some(offset) = offset { + parsed.set_offset(i64::from(offset)) + } else { + parsed.set_offset(NO_OFFSET_INFO) + }.map_err(|e| (s, e))?; } &RFC2822 => try_consume!(parse_rfc2822(parsed, s)), @@ -853,7 +870,7 @@ fn test_rfc2822() { ("Tue, 20 Jan 2015 17:35:90 -0800", Err(OUT_OF_RANGE)), // bad second ("Tue, 20 Jan 2015 17:35:20 -0890", Err(OUT_OF_RANGE)), // bad offset ("6 Jun 1944 04:00:00Z", Err(INVALID)), // bad offset (zulu not allowed) - ("Tue, 20 Jan 2015 17:35:20 HAS", Err(NOT_ENOUGH)), // bad named time zone + ("Tue, 20 Jan 2015 17:35:20 HAS", Err(INVALID)), // bad named time zone // named timezones that have specific timezone offsets // see https://www.rfc-editor.org/rfc/rfc2822#section-4.3 ("Tue, 20 Jan 2015 17:35:20 GMT", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), @@ -875,7 +892,7 @@ fn test_rfc2822() { ("Tue, 20 Jan 2015 17:35:20 K", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), ("Tue, 20 Jan 2015 17:35:20 k", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), // named single-letter timezone "J" is specifically not valid - ("Tue, 20 Jan 2015 17:35:20 J", Err(NOT_ENOUGH)), + ("Tue, 20 Jan 2015 17:35:20 J", Err(INVALID)), ]; fn rfc2822_to_datetime(date: &str) -> ParseResult> { diff --git a/src/format/parsed.rs b/src/format/parsed.rs index b5fb8e699f..df3dcbbd6d 100644 --- a/src/format/parsed.rs +++ b/src/format/parsed.rs @@ -111,6 +111,8 @@ pub struct Parsed { _dummy: (), } +pub const NO_OFFSET_INFO: i64 = i32::MIN as i64; + /// Checks if `old` is either empty or has the same value as `new` (i.e. "consistent"), /// and if it is empty, set `old` to `new` as well. #[inline] @@ -615,7 +617,12 @@ impl Parsed { /// Returns a parsed fixed time zone offset out of given fields. pub fn to_fixed_offset(&self) -> ParseResult { - self.offset.and_then(FixedOffset::east_opt).ok_or(OUT_OF_RANGE) + let offset = self.offset.ok_or(NOT_ENOUGH)?; + if offset == NO_OFFSET_INFO as i32 { + Ok(FixedOffset::NO_OFFSET) + } else { + FixedOffset::east_opt(offset).ok_or(OUT_OF_RANGE) + } } /// Returns a parsed timezone-aware date and time out of given fields. diff --git a/src/format/scan.rs b/src/format/scan.rs index 705ccf9095..88edab2902 100644 --- a/src/format/scan.rs +++ b/src/format/scan.rs @@ -207,7 +207,9 @@ pub(super) fn colon_or_space(s: &str) -> ParseResult<&str> { /// /// 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. -pub(super) fn timezone_offset(s: &str, consume_colon: F) -> ParseResult<(&str, i32)> +/// +/// May return `None` which indicates no offset data is available (i.e. `-0000`). +pub(super) fn timezone_offset(s: &str, consume_colon: F) -> ParseResult<(&str, Option)> where F: FnMut(&str) -> ParseResult<&str>, { @@ -218,7 +220,7 @@ fn timezone_offset_internal( mut s: &str, mut consume_colon: F, allow_missing_minutes: bool, -) -> ParseResult<(&str, i32)> +) -> ParseResult<(&str, Option)> where F: FnMut(&str) -> ParseResult<&str>, { @@ -268,22 +270,27 @@ where }; let seconds = hours * 3600 + minutes * 60; - Ok((s, if negative { -seconds } else { seconds })) + + if seconds == 0 && negative { + return Ok((s, None)); + } + Ok((s, Some(if negative { -seconds } else { seconds }))) } /// Same as `timezone_offset` but also allows for `z`/`Z` which is the same as `+00:00`. -pub(super) fn timezone_offset_zulu(s: &str, colon: F) -> ParseResult<(&str, i32)> +/// May return `None` which indicates no offset data is available (i.e. `-0000`). +pub(super) fn timezone_offset_zulu(s: &str, colon: F) -> ParseResult<(&str, Option)> where F: FnMut(&str) -> ParseResult<&str>, { let bytes = s.as_bytes(); match bytes.first() { - Some(&b'z') | Some(&b'Z') => Ok((&s[1..], 0)), + Some(&b'z') | Some(&b'Z') => Ok((&s[1..], Some(0))), Some(&b'u') | Some(&b'U') => { if bytes.len() >= 3 { let (b, c) = (bytes[1], bytes[2]); match (b | 32, c | 32) { - (b't', b'c') => Ok((&s[3..], 0)), + (b't', b'c') => Ok((&s[3..], Some(0))), _ => Err(INVALID), } } else { @@ -296,18 +303,18 @@ where /// Same as `timezone_offset` but also allows for `z`/`Z` which is the same as /// `+00:00`, and allows missing minutes entirely. -pub(super) fn timezone_offset_permissive(s: &str, colon: F) -> ParseResult<(&str, i32)> +pub(super) fn timezone_offset_permissive(s: &str, colon: F) -> ParseResult<(&str, Option)> where F: FnMut(&str) -> ParseResult<&str>, { match s.as_bytes().first() { - Some(&b'z') | Some(&b'Z') => Ok((&s[1..], 0)), + Some(&b'z') | Some(&b'Z') => Ok((&s[1..], Some(0))), _ => timezone_offset_internal(s, colon, true), } } /// Same as `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 no offset data is available (i.e. `-0000`). /// See [RFC 2822 Section 4.3]. /// /// [RFC 2822 Section 4.3]: https://tools.ietf.org/html/rfc2822#section-4.3 @@ -318,30 +325,27 @@ pub(super) fn timezone_offset_2822(s: &str) -> ParseResult<(&str, Option)> let name = &s.as_bytes()[..upto]; let s = &s[upto..]; let offset_hours = |o| Ok((s, Some(o * 3600))); - if equals(name, "gmt") || equals(name, "ut") { - offset_hours(0) + if equals(name, "gmt") || equals(name, "ut") || equals(name, "z") || equals(name, "Z") { + return offset_hours(0); } else if equals(name, "edt") { - offset_hours(-4) + return offset_hours(-4); } else if equals(name, "est") || equals(name, "cdt") { - offset_hours(-5) + return offset_hours(-5); } else if equals(name, "cst") || equals(name, "mdt") { - offset_hours(-6) + return offset_hours(-6); } else if equals(name, "mst") || equals(name, "pdt") { - offset_hours(-7) + return offset_hours(-7); } else if equals(name, "pst") { - offset_hours(-8) + return offset_hours(-8); } else if name.len() == 1 { - match name[0] { + if let b'a'..=b'i' | b'k'..=b'y' | b'A'..=b'I' | b'K'..=b'Y' = name[0] { // recommended by RFC 2822: consume but treat it as -0000 - b'a'..=b'i' | b'k'..=b'z' | b'A'..=b'I' | b'K'..=b'Z' => offset_hours(0), - _ => Ok((s, None)), + return Ok((s, None)); } - } else { - Ok((s, None)) } + Err(INVALID) } else { - let (s_, offset) = timezone_offset(s, |s| Ok(s))?; - Ok((s_, Some(offset))) + timezone_offset(s, |s| Ok(s)) } } diff --git a/src/offset/fixed.rs b/src/offset/fixed.rs index 04ffa3b989..dfa60c9aa8 100644 --- a/src/offset/fixed.rs +++ b/src/offset/fixed.rs @@ -22,12 +22,26 @@ use crate::Timelike; /// on a `FixedOffset` struct is the preferred way to construct /// `DateTime` instances. See the [`east_opt`](#method.east_opt) and /// [`west_opt`](#method.west_opt) methods for examples. -#[derive(PartialEq, Eq, Hash, Copy, Clone)] +#[derive(Eq, Hash, Copy, Clone)] #[cfg_attr(feature = "rkyv", derive(Archive, Deserialize, Serialize))] pub struct FixedOffset { local_minus_utc: i32, } +pub(crate) const NO_OFFSET: i32 = i32::MIN; + +impl PartialEq for FixedOffset { + fn eq(&self, other: &Self) -> bool { + if (self.local_minus_utc == NO_OFFSET && other.local_minus_utc == 0) + || (self.local_minus_utc == 0 && other.local_minus_utc == NO_OFFSET) + { + true + } else { + self.local_minus_utc == other.local_minus_utc + } + } +} + impl FixedOffset { /// Makes a new `FixedOffset` for the Eastern Hemisphere with given timezone difference. /// The negative `secs` means the Western Hemisphere. @@ -98,14 +112,29 @@ impl FixedOffset { /// Returns the number of seconds to add to convert from UTC to the local time. #[inline] pub const fn local_minus_utc(&self) -> i32 { - self.local_minus_utc + if self.local_minus_utc == NO_OFFSET { + 0 + } else { + self.local_minus_utc + } } /// Returns the number of seconds to add to convert from the local time to UTC. #[inline] pub const fn utc_minus_local(&self) -> i32 { - -self.local_minus_utc + -self.local_minus_utc() + } + + /// Returns true if this `FixedOffset` contains no offset data (in some formats encoded as + /// `-00:00`). + #[inline] + pub const fn no_offset_info(&self) -> bool { + self.local_minus_utc == NO_OFFSET } + + /// A special value to indicate no offset information is available. + /// The created offset will have the value `-00:00`. + pub const NO_OFFSET: FixedOffset = FixedOffset { local_minus_utc: i32::MIN }; } impl TimeZone for FixedOffset { @@ -138,8 +167,11 @@ impl Offset for FixedOffset { impl fmt::Debug for FixedOffset { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let offset = self.local_minus_utc; - let (sign, offset) = if offset < 0 { ('-', -offset) } else { ('+', offset) }; + if self.no_offset_info() { + return write!(f, "-00:00"); + } + let offset = self.local_minus_utc(); + let (sign, offset) = if self.local_minus_utc < 0 { ('-', -offset) } else { ('+', offset) }; let (mins, sec) = div_mod_floor(offset, 60); let (hour, min) = div_mod_floor(mins, 60); if sec == 0 { @@ -186,7 +218,7 @@ impl Add for NaiveTime { #[inline] fn add(self, rhs: FixedOffset) -> NaiveTime { - add_with_leapsecond(&self, rhs.local_minus_utc) + add_with_leapsecond(&self, rhs.local_minus_utc()) } } @@ -195,7 +227,7 @@ impl Sub for NaiveTime { #[inline] fn sub(self, rhs: FixedOffset) -> NaiveTime { - add_with_leapsecond(&self, -rhs.local_minus_utc) + add_with_leapsecond(&self, -rhs.local_minus_utc()) } } @@ -204,7 +236,7 @@ impl Add for NaiveDateTime { #[inline] fn add(self, rhs: FixedOffset) -> NaiveDateTime { - add_with_leapsecond(&self, rhs.local_minus_utc) + add_with_leapsecond(&self, rhs.local_minus_utc()) } } @@ -213,7 +245,7 @@ impl Sub for NaiveDateTime { #[inline] fn sub(self, rhs: FixedOffset) -> NaiveDateTime { - add_with_leapsecond(&self, -rhs.local_minus_utc) + add_with_leapsecond(&self, -rhs.local_minus_utc()) } } @@ -222,7 +254,7 @@ impl Add for DateTime { #[inline] fn add(self, rhs: FixedOffset) -> DateTime { - add_with_leapsecond(&self, rhs.local_minus_utc) + add_with_leapsecond(&self, rhs.local_minus_utc()) } } @@ -231,7 +263,7 @@ impl Sub for DateTime { #[inline] fn sub(self, rhs: FixedOffset) -> DateTime { - add_with_leapsecond(&self, -rhs.local_minus_utc) + add_with_leapsecond(&self, -rhs.local_minus_utc()) } } diff --git a/src/offset/utc.rs b/src/offset/utc.rs index aeaeb672dc..d79885fc0e 100644 --- a/src/offset/utc.rs +++ b/src/offset/utc.rs @@ -111,7 +111,7 @@ impl TimeZone for Utc { impl Offset for Utc { fn fix(&self) -> FixedOffset { - FixedOffset::east_opt(0).unwrap() + FixedOffset::NO_OFFSET } }