diff --git a/mononoke-types/if/mononoke_types_thrift.thrift b/mononoke-types/if/mononoke_types_thrift.thrift index 55f8c5e07c..117b73e494 100644 --- a/mononoke-types/if/mononoke_types_thrift.thrift +++ b/mononoke-types/if/mononoke_types_thrift.thrift @@ -40,3 +40,10 @@ typedef Blake2 ContentId (hs.newtype) // manifests can be applied in a streaming way. typedef binary MPathElement (hs.newtype) typedef list MPath (hs.newtype) + +struct DateTime { + 1: required i64 timestamp_secs, + // Timezones can go up to UTC+13 (which would be represented as -46800), so + // an i16 can't fit them. + 2: required i32 tz_offset_secs, +} diff --git a/mononoke-types/mocks/datetime.rs b/mononoke-types/mocks/datetime.rs new file mode 100644 index 0000000000..ae0a1e8c05 --- /dev/null +++ b/mononoke-types/mocks/datetime.rs @@ -0,0 +1,43 @@ +// Copyright (c) 2018-present, Facebook, Inc. +// All Rights Reserved. +// +// This software may be used and distributed according to the terms of the +// GNU General Public License version 2 or any later version. + +use chrono::{FixedOffset, TimeZone}; + +use mononoke_types::DateTime; + +/// Return a `DateTime` corresponding to -01-01 00:00:00 UTC. +pub fn day_1_utc(year: i32) -> DateTime { + DateTime::new(FixedOffset::west(0).ymd(year, 1, 1).and_hms(0, 0, 0)) +} + +/// Return a `DateTime` corresponding to -01-01 00:00:00 UTC, +/// with the specified offset applied. +pub fn day_1_tz(year: i32, offset: i32) -> DateTime { + DateTime::new(FixedOffset::west(offset).ymd(year, 1, 1).and_hms(0, 0, 0)) +} + +pub const PST_OFFSET: i32 = 7 * 3600; + +lazy_static! { + /// 1970-01-01 00:00:00 UTC. + pub static ref EPOCH_ZERO: DateTime = DateTime::from_timestamp(0, 0).unwrap(); + /// 1970-01-01 00:00:00 UTC-07. + pub static ref EPOCH_ZERO_PST: DateTime = DateTime::from_timestamp(0, PST_OFFSET).unwrap(); + + /// 1900-01-01 00:00:00 UTC. + pub static ref YEAR_1900: DateTime = day_1_utc(1900); + /// 1900-01-01 00:00:00 UTC-07. + pub static ref YEAR_1900_PST: DateTime = day_1_tz(1900, PST_OFFSET); + + /// 2000-01-01 00:00:00 UTC. + pub static ref YEAR_2000: DateTime = day_1_utc(2000); + /// 2000-01-01 00:00:00 UTC-07. + pub static ref YEAR_2000_PST: DateTime = day_1_tz(2000, PST_OFFSET); + + /// 2100-01-01 00:00:00 UTC. + pub static ref YEAR_2100: DateTime = day_1_utc(2000); + pub static ref YEAR_2100_PST: DateTime = day_1_tz(2100, PST_OFFSET); +} diff --git a/mononoke-types/mocks/lib.rs b/mononoke-types/mocks/lib.rs index 3baffb822f..bb142df95d 100644 --- a/mononoke-types/mocks/lib.rs +++ b/mononoke-types/mocks/lib.rs @@ -7,9 +7,14 @@ #![deny(warnings)] #![feature(const_fn)] +extern crate chrono; +#[macro_use] +extern crate lazy_static; + extern crate mononoke_types; pub mod changesetid; pub mod contentid; +pub mod datetime; pub mod hash; pub mod unodeid; diff --git a/mononoke-types/src/datetime.rs b/mononoke-types/src/datetime.rs new file mode 100644 index 0000000000..67bae05e84 --- /dev/null +++ b/mononoke-types/src/datetime.rs @@ -0,0 +1,142 @@ +// Copyright (c) 2018-present, Facebook, Inc. +// All Rights Reserved. +// +// This software may be used and distributed according to the terms of the +// GNU General Public License version 2 or any later version. + +use std::fmt::{self, Display}; + +use chrono::{DateTime as ChronoDateTime, FixedOffset, LocalResult, TimeZone}; +use quickcheck::{empty_shrinker, Arbitrary, Gen}; + +use errors::*; +use thrift; + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct DateTime(ChronoDateTime); + +impl DateTime { + #[inline] + pub fn new(dt: ChronoDateTime) -> Self { + DateTime(dt) + } + + pub fn from_timestamp(secs: i64, tz_offset_secs: i32) -> Result { + let tz = FixedOffset::west_opt(tz_offset_secs).ok_or_else(|| { + ErrorKind::InvalidDateTime(format!("timezone offset out of range: {}", tz_offset_secs)) + })?; + let dt = match tz.timestamp_opt(secs, 0) { + LocalResult::Single(dt) => dt, + _ => bail_err!(ErrorKind::InvalidDateTime(format!( + "seconds out of range: {}", + secs + ))), + }; + Ok(Self::new(dt)) + } + + pub(crate) fn from_thrift(dt: thrift::DateTime) -> Result { + Self::from_timestamp(dt.timestamp_secs, dt.tz_offset_secs) + } + + /// Retrieves the Unix timestamp in UTC. + #[inline] + pub fn timestamp_secs(&self) -> i64 { + self.0.timestamp() + } + + /// Retrieves the timezone offset, as represented by the number of seconds to + /// add to convert local time to UTC. + #[inline] + pub fn tz_offset_secs(&self) -> i32 { + // This is the same as the way Mercurial stores timezone offsets. + self.0.offset().utc_minus_local() + } + + #[inline] + pub fn as_chrono(&self) -> &ChronoDateTime { + &self.0 + } + + #[inline] + pub fn into_chrono(self) -> ChronoDateTime { + self.0 + } + + pub(crate) fn into_thrift(self) -> thrift::DateTime { + thrift::DateTime { + timestamp_secs: self.timestamp_secs(), + tz_offset_secs: self.tz_offset_secs(), + } + } +} + +impl Display for DateTime { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{}", self.0) + } +} + +impl Arbitrary for DateTime { + fn arbitrary(g: &mut G) -> Self { + // Ensure a large domain from which to get second values. + let secs = g.gen_range(i32::min_value(), i32::max_value()) as i64; + // Timezone offsets in the range [-86399, 86399] (both inclusive) are valid. + // gen_range generates a value in the range [low, high). + let tz_offset_secs = g.gen_range(-86_399, 86_400); + DateTime::from_timestamp(secs, tz_offset_secs) + .expect("Arbitrary instances should always be valid") + } + + fn shrink(&self) -> Box> { + empty_shrinker() + } +} + +#[cfg(test)] +mod test { + use super::*; + + quickcheck! { + fn thrift_roundtrip(dt: DateTime) -> bool { + let thrift_dt = dt.into_thrift(); + let dt2 = DateTime::from_thrift(thrift_dt) + .expect("roundtrip instances should always be valid"); + // Equality on DateTime structs doesn't pay attention to the time zone, + // in order to be consistent with Ord. + dt == dt2 && dt.tz_offset_secs() == dt2.tz_offset_secs() + } + } + + #[test] + fn bad_inputs() { + DateTime::from_timestamp(0, 86_400) + .expect_err("unexpected OK - tz_offset_secs out of bounds"); + DateTime::from_timestamp(0, -86_400) + .expect_err("unexpected OK - tz_offset_secs out of bounds"); + DateTime::from_timestamp(i64::min_value(), 0) + .expect_err("unexpected OK - timestamp_secs out of bounds"); + DateTime::from_timestamp(i64::max_value(), 0) + .expect_err("unexpected OK - timestamp_secs out of bounds"); + } + + #[test] + fn bad_thrift() { + DateTime::from_thrift(thrift::DateTime { + timestamp_secs: 0, + tz_offset_secs: 86_400, + }).expect_err("unexpected OK - tz_offset_secs out of bounds"); + DateTime::from_thrift(thrift::DateTime { + timestamp_secs: 0, + tz_offset_secs: -86_400, + }).expect_err("unexpected OK - tz_offset_secs out of bounds"); + DateTime::from_thrift(thrift::DateTime { + timestamp_secs: i64::min_value(), + tz_offset_secs: 0, + }).expect_err("unexpected OK - timestamp_secs out of bounds"); + DateTime::from_thrift(thrift::DateTime { + timestamp_secs: i64::max_value(), + tz_offset_secs: 0, + }).expect_err("unexpected OK - timestamp_secs out of bounds"); + } +} diff --git a/mononoke-types/src/errors.rs b/mononoke-types/src/errors.rs index 99e5c8490b..d4a7c350f8 100644 --- a/mononoke-types/src/errors.rs +++ b/mononoke-types/src/errors.rs @@ -14,6 +14,7 @@ pub enum ErrorKind { #[fail(display = "invalid path '{}': {}", _0, _1)] InvalidPath(String, String), #[fail(display = "invalid Mononoke path '{}': {}", _0, _1)] InvalidMPath(MPath, String), #[fail(display = "invalid Thrift structure '{}': {}", _0, _1)] InvalidThrift(String, String), + #[fail(display = "invalid changeset date: {}", _0)] InvalidDateTime(String), } pub type Result = ::std::result::Result; diff --git a/mononoke-types/src/lib.rs b/mononoke-types/src/lib.rs index 2319d365ce..1db35aaf97 100644 --- a/mononoke-types/src/lib.rs +++ b/mononoke-types/src/lib.rs @@ -18,6 +18,7 @@ extern crate ascii; extern crate assert_matches; extern crate bincode; extern crate blake2; +extern crate chrono; #[macro_use] extern crate failure_ext as failure; extern crate heapsize; @@ -35,11 +36,13 @@ extern crate serde_derive; extern crate mononoke_types_thrift; +pub mod datetime; pub mod errors; pub mod hash; pub mod path; pub mod typed_hash; +pub use datetime::DateTime; pub use path::{MPath, MPathElement, RepoPath}; pub use typed_hash::{ChangesetId, ContentId, UnodeId};