diff --git a/src/datetime.rs b/src/datetime.rs index eb4166c..4679e38 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -5,7 +5,7 @@ use std::{str, fmt, hash}; use std::cmp::Ordering; -use std::ops::{Add, Sub}; +use std::ops::{Add, Sub, Deref}; use oldtime::Duration as OldDuration; use {Weekday, Timelike, Datelike}; @@ -30,6 +30,26 @@ pub struct DateTime { offset: Tz::Offset, } +/// A DateTime that can be deserialized from a timestamp +/// +/// A timestamp here is seconds since the epoch +pub struct TsSeconds(DateTime); + +impl From> for DateTime { + /// Pull the inner DateTime out + fn from(obj: TsSeconds) -> DateTime { + obj.0 + } +} + +impl Deref for TsSeconds { + type Target = DateTime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl DateTime { /// Makes a new `DateTime` with given *UTC* datetime and offset. /// The local datetime should be constructed via the `TimeZone` trait. @@ -432,40 +452,61 @@ fn test_decodable_json(utc_from_str: FUTC, norm(&Some(UTC.ymd(2014, 7, 24).and_hms(12, 34, 6)))); assert_eq!(norm(&utc_from_str(r#""2014-07-24T13:57:06+01:23""#).ok()), norm(&Some(UTC.ymd(2014, 7, 24).and_hms(12, 34, 6)))); - assert_eq!(norm(&utc_from_str("0").ok()), - norm(&Some(UTC.ymd(1970, 1, 1).and_hms(0, 0, 0)))); - assert_eq!(norm(&utc_from_str("-1").unwrap()), - norm(&UTC.ymd(1969, 12, 31).and_hms(23, 59, 59))); - assert_eq!(norm(&fixed_from_str(r#""2014-07-24T12:34:06Z""#).ok()), norm(&Some(FixedOffset::east(0).ymd(2014, 7, 24).and_hms(12, 34, 6)))); assert_eq!(norm(&fixed_from_str(r#""2014-07-24T13:57:06+01:23""#).ok()), norm(&Some(FixedOffset::east(60*60 + 23*60).ymd(2014, 7, 24).and_hms(13, 57, 6)))); - assert_eq!(norm(&fixed_from_str("0").ok()), - norm(&Some(UTC.ymd(1970, 1, 1).and_hms(0, 0, 0)))); - assert_eq!(norm(&fixed_from_str("-1").unwrap()), - norm(&UTC.ymd(1969, 12, 31).and_hms(23, 59, 59))); // we don't know the exact local offset but we can check that // the conversion didn't change the instant itself - assert_eq!(local_from_str(r#""2014-07-24T12:34:06Z""#).unwrap(), + assert_eq!(local_from_str(r#""2014-07-24T12:34:06Z""#) + .expect("local shouuld parse"), UTC.ymd(2014, 7, 24).and_hms(12, 34, 6)); - assert_eq!(local_from_str(r#""2014-07-24T13:57:06+01:23""#).unwrap(), + assert_eq!(local_from_str(r#""2014-07-24T13:57:06+01:23""#) + .expect("local should parse with offset"), UTC.ymd(2014, 7, 24).and_hms(12, 34, 6)); - assert_eq!(fixed_from_str("0").unwrap(), - UTC.ymd(1970, 1, 1).and_hms(0, 0, 0)); - assert_eq!(local_from_str("-1").unwrap(), - &UTC.ymd(1969, 12, 31).and_hms(23, 59, 59)); assert!(utc_from_str(r#""2014-07-32T12:34:06Z""#).is_err()); assert!(fixed_from_str(r#""2014-07-32T12:34:06Z""#).is_err()); } + + +#[cfg(all(test, any(feature = "rustc-serialize", feature = "serde")))] +fn test_decodable_json_timestamps(utc_from_str: FUTC, + fixed_from_str: FFixed, + local_from_str: FLocal) + where FUTC: Fn(&str) -> Result, E>, + FFixed: Fn(&str) -> Result, E>, + FLocal: Fn(&str) -> Result, E>, + E: ::std::fmt::Debug +{ + fn norm(dt: &Option>) -> Option<(&DateTime, &Tz::Offset)> { + dt.as_ref().map(|dt| (dt, dt.offset())) + } + + assert_eq!(norm(&utc_from_str("0").ok().map(DateTime::from)), + norm(&Some(UTC.ymd(1970, 1, 1).and_hms(0, 0, 0)))); + assert_eq!(norm(&utc_from_str("-1").ok().map(DateTime::from)), + norm(&Some(UTC.ymd(1969, 12, 31).and_hms(23, 59, 59)))); + + assert_eq!(norm(&fixed_from_str("0").ok().map(DateTime::from)), + norm(&Some(FixedOffset::east(0).ymd(1970, 1, 1).and_hms(0, 0, 0)))); + assert_eq!(norm(&fixed_from_str("-1").ok().map(DateTime::from)), + norm(&Some(FixedOffset::east(0).ymd(1969, 12, 31).and_hms(23, 59, 59)))); + + assert_eq!(*fixed_from_str("0").expect("0 timestamp should parse"), + UTC.ymd(1970, 1, 1).and_hms(0, 0, 0)); + assert_eq!(*local_from_str("-1").expect("-1 timestamp should parse"), + UTC.ymd(1969, 12, 31).and_hms(23, 59, 59)); +} + #[cfg(feature = "rustc-serialize")] mod rustc_serialize { - use super::DateTime; - use offset::TimeZone; + use std::fmt; + use super::{DateTime, TsSeconds}; + use offset::{TimeZone, LocalResult}; use offset::utc::UTC; use offset::local::Local; use offset::fixed::FixedOffset; @@ -477,21 +518,47 @@ mod rustc_serialize { } } + // try!-like function to convert a LocalResult into a serde-ish Result + fn from(me: LocalResult, d: &mut D) -> Result + where D: Decoder, + T: fmt::Display, + { + match me { + LocalResult::None => Err(d.error( + "value is not a legal timestamp")), + LocalResult::Ambiguous(..) => Err(d.error( + "value is an ambiguous timestamp")), + LocalResult::Single(val) => Ok(val) + } + } + impl Decodable for DateTime { fn decode(d: &mut D) -> Result, D::Error> { - match d.read_str()?.parse::>() { - Ok(dt) => Ok(dt), - Err(_) => Err(d.error("invalid date and time")), - } + d.read_str()?.parse::>() + .map_err(|_| d.error("invalid date and time")) + } + } + + impl Decodable for TsSeconds { + fn decode(d: &mut D) -> Result, D::Error> { + from(FixedOffset::east(0).timestamp_opt(d.read_i64()?, 0), d) + .map(|dt| TsSeconds(dt)) } } impl Decodable for DateTime { fn decode(d: &mut D) -> Result, D::Error> { - match d.read_str()?.parse::>() { - Ok(dt) => Ok(dt.with_timezone(&UTC)), - Err(_) => Err(d.error("invalid date and time")), - } + d.read_str()? + .parse::>() + .map(|dt| dt.with_timezone(&UTC)) + .map_err(|_| d.error("invalid date and time")) + } + } + + impl Decodable for TsSeconds { + fn decode(d: &mut D) -> Result, D::Error> { + from(UTC.timestamp_opt(d.read_i64()?, 0), d) + .map(|dt| TsSeconds(dt)) } } @@ -504,6 +571,13 @@ mod rustc_serialize { } } + impl Decodable for TsSeconds { + fn decode(d: &mut D) -> Result, D::Error> { + from(UTC.timestamp_opt(d.read_i64()?, 0), d) + .map(|dt| TsSeconds(dt.with_timezone(&Local))) + } + } + #[cfg(test)] use rustc_serialize::json; #[test] @@ -515,12 +589,18 @@ mod rustc_serialize { fn test_decodable() { super::test_decodable_json(json::decode, json::decode, json::decode); } + + #[test] + fn test_decodable_timestamps() { + super::test_decodable_json_timestamps(json::decode, json::decode, json::decode); + } + } #[cfg(feature = "serde")] mod serde { use std::fmt; - use super::DateTime; + use super::{DateTime, TsSeconds}; use offset::{TimeZone, LocalResult}; use offset::utc::UTC; use offset::local::Local; @@ -551,8 +631,8 @@ mod serde { // try!-like function to convert a LocalResult into a serde-ish Result fn from(me: LocalResult, ts: V) -> Result where E: de::Error, - V: Display, - T: Display, + V: fmt::Display, + T: fmt::Display, { match me { LocalResult::None => Err(E::custom( @@ -579,21 +659,6 @@ mod serde { { value.parse().map_err(|err| E::custom(format!("{}", err))) } - - // Deserialize a timestamp in seconds since the epoch - fn visit_i64(self, value: i64) -> Result, E> - where E: de::Error - { - from(FixedOffset::east(0).timestamp_opt(value, 0), value) - } - - // Deserialize a timestamp in seconds since the epoch - fn visit_u64(self, value: u64) -> Result, E> - where E: de::Error - { - from(FixedOffset::east(0).timestamp_opt(value as i64, 0), value) - } - } /// Deserialize a value that optionally includes a timezone offset in its @@ -634,6 +699,65 @@ mod serde { } } + struct SecondsTimestampVisitor; + + impl de::Visitor for SecondsTimestampVisitor { + type Value = DateTime; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result + { + write!(formatter, "a unix timestamp in seconds") + } + + /// Deserialize a timestamp in seconds since the epoch + fn visit_i64(self, value: i64) -> Result, E> + where E: de::Error + { + from(FixedOffset::east(0).timestamp_opt(value, 0), value) + } + + /// Deserialize a timestamp in seconds since the epoch + fn visit_u64(self, value: u64) -> Result, E> + where E: de::Error + { + from(FixedOffset::east(0).timestamp_opt(value as i64, 0), value) + } + } + + impl de::Deserialize for TsSeconds { + fn deserialize(deserializer: D) -> Result + where D: de::Deserializer + { + Ok(TsSeconds(try!( + deserializer + .deserialize_str(SecondsTimestampVisitor) + .map(|dt| dt.with_timezone(&Local))))) + } + } + + /// Can deserialize a timestamp into a FixedOffset + /// + /// The offset will always be 0, because timestamps are defined as UTC. + impl de::Deserialize for TsSeconds { + fn deserialize(deserializer: D) -> Result + where D: de::Deserializer + { + Ok(TsSeconds(try!( + deserializer.deserialize_str(SecondsTimestampVisitor)))) + } + } + + /// Deserialize into a UTC value + impl de::Deserialize for TsSeconds { + fn deserialize(deserializer: D) -> Result + where D: de::Deserializer + { + Ok(TsSeconds(try!( + deserializer.deserialize_str(SecondsTimestampVisitor) + .map(|dt| dt.with_timezone(&UTC))))) + } + } + #[cfg(test)] extern crate serde_json; #[cfg(test)] extern crate bincode; @@ -648,6 +772,13 @@ mod serde { |input| self::serde_json::from_str(&input)); } + #[test] + fn test_serde_deserialize_timestamps() { + super::test_decodable_json_timestamps(self::serde_json::from_str, + self::serde_json::from_str, + self::serde_json::from_str); + } + #[test] fn test_serde_bincode() { // Bincode is relevant to test separately from JSON because diff --git a/src/naive/datetime.rs b/src/naive/datetime.rs index 9d00c2f..a6f7604 100644 --- a/src/naive/datetime.rs +++ b/src/naive/datetime.rs @@ -4,7 +4,7 @@ //! ISO 8601 date and time without timezone. use std::{str, fmt, hash}; -use std::ops::{Add, Sub}; +use std::ops::{Add, Sub, Deref}; use num::traits::ToPrimitive; use oldtime::Duration as OldDuration; @@ -54,6 +54,24 @@ pub struct NaiveDateTime { time: NaiveTime, } +/// A DateTime that can be deserialized from a seconds-based timestamp +pub struct TsSeconds(NaiveDateTime); + +impl From for NaiveDateTime { + /// Pull the internal NaiveDateTime out + fn from(obj: TsSeconds) -> NaiveDateTime { + obj.0 + } +} + +impl Deref for TsSeconds { + type Target = NaiveDateTime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl NaiveDateTime { /// Makes a new `NaiveDateTime` from date and time components. /// Equivalent to [`date.and_time(time)`](../date/struct.NaiveDate.html#method.and_time) @@ -1402,16 +1420,6 @@ fn test_decodable_json(from_str: F) assert_eq!( from_str(r#""+262143-12-31T23:59:60.9999999999997""#).ok(), // excess digits are ignored Some(date::MAX.and_hms_nano(23, 59, 59, 1_999_999_999))); - assert_eq!( - from_str("0").unwrap(), - NaiveDate::from_ymd(1970, 1, 1).and_hms(0, 0, 0), - "should parse integers as timestamps" - ); - assert_eq!( - from_str("-1").unwrap(), - NaiveDate::from_ymd(1969, 12, 31).and_hms(23, 59, 59), - "should parse integers as timestamps" - ); // bad formats assert!(from_str(r#""""#).is_err()); @@ -1428,7 +1436,6 @@ fn test_decodable_json(from_str: F) assert!(from_str(r#""2016-07-08 09:10:48.090""#).is_err()); assert!(from_str(r#""2016-007-08T09:10:48.090""#).is_err()); assert!(from_str(r#""yyyy-mm-ddThh:mm:ss.fffffffff""#).is_err()); - assert!(from_str(r#"0"#).is_err()); assert!(from_str(r#"20160708000000"#).is_err()); assert!(from_str(r#"{}"#).is_err()); // pre-0.3.0 rustc-serialize format is now invalid @@ -1436,9 +1443,27 @@ fn test_decodable_json(from_str: F) assert!(from_str(r#"null"#).is_err()); } + +#[cfg(all(test, any(feature = "rustc-serialize", feature = "serde")))] +fn test_decodable_json_timestamp(from_str: F) + where F: Fn(&str) -> Result, E: ::std::fmt::Debug +{ + assert_eq!( + *from_str("0").unwrap(), + NaiveDate::from_ymd(1970, 1, 1).and_hms(0, 0, 0), + "should parse integers as timestamps" + ); + assert_eq!( + *from_str("-1").unwrap(), + NaiveDate::from_ymd(1969, 12, 31).and_hms(23, 59, 59), + "should parse integers as timestamps" + ); +} + + #[cfg(feature = "rustc-serialize")] mod rustc_serialize { - use super::NaiveDateTime; + use super::{NaiveDateTime, TsSeconds}; use rustc_serialize::{Encodable, Encoder, Decodable, Decoder}; impl Encodable for NaiveDateTime { @@ -1449,7 +1474,15 @@ mod rustc_serialize { impl Decodable for NaiveDateTime { fn decode(d: &mut D) -> Result { - d.read_str()?.parse().map_err(|_| d.error("invalid date and time")) + d.read_str()?.parse().map_err(|_| d.error("invalid date time string")) + } + } + + impl Decodable for TsSeconds { + fn decode(d: &mut D) -> Result { + Ok(TsSeconds( + NaiveDateTime::from_timestamp_opt(d.read_i64()?, 0) + .ok_or_else(|| d.error("invalid timestamp"))?)) } } @@ -1464,12 +1497,17 @@ mod rustc_serialize { fn test_decodable() { super::test_decodable_json(json::decode); } + + #[test] + fn test_decodable_timestamps() { + super::test_decodable_json_timestamp(json::decode); + } } #[cfg(feature = "serde")] mod serde { use std::fmt; - use super::NaiveDateTime; + use super::{NaiveDateTime, TsSeconds}; use serde::{ser, de}; // TODO not very optimized for space (binary formats would want something better) @@ -1499,7 +1537,7 @@ mod serde { fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "a formatted date and time string or a unix timestamp") + write!(formatter, "a formatted date and time string") } fn visit_str(self, value: &str) -> Result @@ -1507,6 +1545,25 @@ mod serde { { value.parse().map_err(|err| E::custom(format!("{}", err))) } + } + + impl de::Deserialize for NaiveDateTime { + fn deserialize(deserializer: D) -> Result + where D: de::Deserializer + { + deserializer.deserialize_str(NaiveDateTimeVisitor) + } + } + + struct NaiveDateTimeFromSecondsVisitor; + + impl de::Visitor for NaiveDateTimeFromSecondsVisitor { + type Value = NaiveDateTime; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result + { + write!(formatter, "a unix timestamp") + } fn visit_i64(self, value: i64) -> Result where E: de::Error @@ -1527,7 +1584,8 @@ mod serde { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de> { - deserializer.deserialize_str(NaiveDateTimeVisitor) + Ok(TsSeconds(try!( + deserializer.deserialize_str(NaiveDateTimeFromSecondsVisitor)))) } } @@ -1544,6 +1602,11 @@ mod serde { super::test_decodable_json(|input| self::serde_json::from_str(&input)); } + #[test] + fn test_serde_deserialize_timestamp() { + super::test_decodable_json_timestamp(self::serde_json::from_str); + } + #[test] fn test_serde_bincode() { // Bincode is relevant to test separately from JSON because