Skip to content

Commit

Permalink
Encode no offset info
Browse files Browse the repository at this point in the history
  • Loading branch information
pitdicker committed May 10, 2023
1 parent f87d548 commit 60c0f59
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 54 deletions.
12 changes: 5 additions & 7 deletions src/datetime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,9 +468,9 @@ impl From<DateTime<Utc>> for DateTime<FixedOffset> {
/// Convert this `DateTime<Utc>` instance into a `DateTime<FixedOffset>` 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<Utc>) -> Self {
src.with_timezone(&FixedOffset::east_opt(0).unwrap())
src.with_timezone(&FixedOffset::NO_OFFSET)
}
}

Expand Down Expand Up @@ -530,9 +530,9 @@ impl From<DateTime<Local>> for DateTime<FixedOffset> {
/// Convert this `DateTime<Local>` instance into a `DateTime<FixedOffset>` 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<Local>) -> Self {
src.with_timezone(&FixedOffset::east_opt(0).unwrap())
src.with_timezone(&FixedOffset::NO_OFFSET)
}
}

Expand Down Expand Up @@ -1277,9 +1277,7 @@ fn test_decodable_json<FUtc, FFixed, FLocal, E>(

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()),
Expand Down
39 changes: 28 additions & 11 deletions src/format/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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, ()))
}
Expand Down Expand Up @@ -428,15 +433,23 @@ 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 => {
let offset = try_consume!(scan::timezone_offset_zulu(
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,
Expand All @@ -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)),
Expand Down Expand Up @@ -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")),
Expand All @@ -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<DateTime<FixedOffset>> {
Expand Down
9 changes: 8 additions & 1 deletion src/format/parsed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -615,7 +617,12 @@ impl Parsed {

/// Returns a parsed fixed time zone offset out of given fields.
pub fn to_fixed_offset(&self) -> ParseResult<FixedOffset> {
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.
Expand Down
50 changes: 27 additions & 23 deletions src/format/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<F>(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<F>(s: &str, consume_colon: F) -> ParseResult<(&str, Option<i32>)>
where
F: FnMut(&str) -> ParseResult<&str>,
{
Expand All @@ -218,7 +220,7 @@ fn timezone_offset_internal<F>(
mut s: &str,
mut consume_colon: F,
allow_missing_minutes: bool,
) -> ParseResult<(&str, i32)>
) -> ParseResult<(&str, Option<i32>)>
where
F: FnMut(&str) -> ParseResult<&str>,
{
Expand Down Expand Up @@ -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<F>(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<F>(s: &str, colon: F) -> ParseResult<(&str, Option<i32>)>
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 {
Expand All @@ -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<F>(s: &str, colon: F) -> ParseResult<(&str, i32)>
pub(super) fn timezone_offset_permissive<F>(s: &str, colon: F) -> ParseResult<(&str, Option<i32>)>
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
Expand All @@ -318,30 +325,27 @@ pub(super) fn timezone_offset_2822(s: &str) -> ParseResult<(&str, Option<i32>)>
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))
}
}

Expand Down
54 changes: 43 additions & 11 deletions src/offset/fixed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,26 @@ use crate::Timelike;
/// on a `FixedOffset` struct is the preferred way to construct
/// `DateTime<FixedOffset>` 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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -186,7 +218,7 @@ impl Add<FixedOffset> 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())
}
}

Expand All @@ -195,7 +227,7 @@ impl Sub<FixedOffset> 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())
}
}

Expand All @@ -204,7 +236,7 @@ impl Add<FixedOffset> 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())
}
}

Expand All @@ -213,7 +245,7 @@ impl Sub<FixedOffset> 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())
}
}

Expand All @@ -222,7 +254,7 @@ impl<Tz: TimeZone> Add<FixedOffset> for DateTime<Tz> {

#[inline]
fn add(self, rhs: FixedOffset) -> DateTime<Tz> {
add_with_leapsecond(&self, rhs.local_minus_utc)
add_with_leapsecond(&self, rhs.local_minus_utc())
}
}

Expand All @@ -231,7 +263,7 @@ impl<Tz: TimeZone> Sub<FixedOffset> for DateTime<Tz> {

#[inline]
fn sub(self, rhs: FixedOffset) -> DateTime<Tz> {
add_with_leapsecond(&self, -rhs.local_minus_utc)
add_with_leapsecond(&self, -rhs.local_minus_utc())
}
}

Expand Down

0 comments on commit 60c0f59

Please sign in to comment.