mononoke-types: add a type to represent datetimes

Summary: Represented as a chrono datetime in Rust and an (i64, i32) pair in Thrift.

Reviewed By: jsgf

Differential Revision: D7325332

fbshipit-source-id: 22c3b17961ffb0b4fdb4e6e8aece3b257b3c718e
This commit is contained in:
Siddharth Agarwal 2018-03-20 08:26:33 -07:00 committed by Facebook Github Bot
parent d33426121b
commit bb6e012ad8
6 changed files with 201 additions and 0 deletions

View File

@ -40,3 +40,10 @@ typedef Blake2 ContentId (hs.newtype)
// manifests can be applied in a streaming way.
typedef binary MPathElement (hs.newtype)
typedef list<MPathElement> 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,
}

View File

@ -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 <year>-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 <year>-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);
}

View File

@ -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;

View File

@ -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<FixedOffset>);
impl DateTime {
#[inline]
pub fn new(dt: ChronoDateTime<FixedOffset>) -> Self {
DateTime(dt)
}
pub fn from_timestamp(secs: i64, tz_offset_secs: i32) -> Result<Self> {
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> {
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<FixedOffset> {
&self.0
}
#[inline]
pub fn into_chrono(self) -> ChronoDateTime<FixedOffset> {
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: Gen>(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<Iterator<Item = Self>> {
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");
}
}

View File

@ -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<T> = ::std::result::Result<T, Error>;

View File

@ -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};