diff --git a/Cargo.toml b/Cargo.toml index 39e924f..3f0097f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "speedate" authors = ["Samuel Colvin "] -version = "0.10.0" +version = "0.11.0" edition = "2021" description = "Fast and simple datetime, date, time and duration parsing" readme = "README.md" diff --git a/src/datetime.rs b/src/datetime.rs index 47f23e3..dccac89 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -236,7 +236,7 @@ impl DateTime { /// assert_eq!(dt.to_string(), "2022-01-01T12:13:14Z"); /// ``` pub fn parse_bytes_rfc3339(bytes: &[u8]) -> Result { - DateTime::parse_bytes_rfc3339_with_config(bytes, TimeConfig::default()) + DateTime::parse_bytes_rfc3339_with_config(bytes, &TimeConfig::default()) } /// Same as `parse_bytes_rfc3339` with with a `TimeConfig` parameter. @@ -251,7 +251,7 @@ impl DateTime { /// ``` /// use speedate::{DateTime, Date, Time, TimeConfig}; /// - /// let dt = DateTime::parse_bytes_rfc3339_with_config(b"2022-01-01T12:13:14Z", TimeConfig::default()).unwrap(); + /// let dt = DateTime::parse_bytes_rfc3339_with_config(b"2022-01-01T12:13:14Z", &TimeConfig::default()).unwrap(); /// assert_eq!( /// dt, /// DateTime { @@ -271,7 +271,7 @@ impl DateTime { /// ); /// assert_eq!(dt.to_string(), "2022-01-01T12:13:14Z"); /// ``` - pub fn parse_bytes_rfc3339_with_config(bytes: &[u8], config: TimeConfig) -> Result { + pub fn parse_bytes_rfc3339_with_config(bytes: &[u8], config: &TimeConfig) -> Result { // First up, parse the full date if we can let date = Date::parse_bytes_partial(bytes)?; @@ -305,7 +305,7 @@ impl DateTime { /// assert_eq!(dt.to_string(), "2022-01-01T12:13:14"); /// ``` pub fn parse_bytes(bytes: &[u8]) -> Result { - DateTime::parse_bytes_with_config(bytes, TimeConfig::default()) + DateTime::parse_bytes_with_config(bytes, &TimeConfig::default()) } /// Same as `DateTime::parse_bytes` but supporting TimeConfig @@ -320,24 +320,24 @@ impl DateTime { /// ``` /// use speedate::{DateTime, Date, Time, TimeConfig}; /// - /// let dt = DateTime::parse_bytes_with_config(b"2022-01-01T12:13:14Z", TimeConfig::default()).unwrap(); + /// let dt = DateTime::parse_bytes_with_config(b"2022-01-01T12:13:14Z", &TimeConfig::default()).unwrap(); /// assert_eq!(dt.to_string(), "2022-01-01T12:13:14Z"); /// ``` - pub fn parse_bytes_with_config(bytes: &[u8], config: TimeConfig) -> Result { + pub fn parse_bytes_with_config(bytes: &[u8], config: &TimeConfig) -> Result { match Self::parse_bytes_rfc3339_with_config(bytes, config) { Ok(d) => Ok(d), Err(e) => match float_parse_bytes(bytes) { - IntFloat::Int(int) => Self::from_timestamp(int, 0), + IntFloat::Int(int) => Self::from_timestamp_with_config(int, 0, config), IntFloat::Float(float) => { let micro = (float.fract() * 1_000_000_f64).round() as u32; - Self::from_timestamp(float.floor() as i64, micro) + Self::from_timestamp_with_config(float.floor() as i64, micro, config) } IntFloat::Err => Err(e), }, } } - /// Create a datetime from a Unix Timestamp in seconds or milliseconds + /// Like `from_timestamp` but with a `TimeConfig`. /// /// ("Unix Timestamp" means number of seconds or milliseconds since 1970-01-01) /// @@ -356,6 +356,7 @@ impl DateTime { /// /// * `timestamp` - timestamp in either seconds or milliseconds /// * `timestamp_microsecond` - microseconds fraction of a second timestamp + /// * `config` - the `TimeConfig` to use /// /// Where `timestamp` is interrupted as milliseconds and is not a whole second, the remainder is added to /// `timestamp_microsecond`. @@ -363,15 +364,19 @@ impl DateTime { /// # Examples /// /// ``` - /// use speedate::DateTime; + /// use speedate::{DateTime, TimeConfig}; /// - /// let d = DateTime::from_timestamp(1_654_619_320, 123).unwrap(); + /// let d = DateTime::from_timestamp_with_config(1_654_619_320, 123, &TimeConfig::default()).unwrap(); /// assert_eq!(d.to_string(), "2022-06-07T16:28:40.000123"); /// - /// let d = DateTime::from_timestamp(1_654_619_320_123, 123_000).unwrap(); + /// let d = DateTime::from_timestamp_with_config(1_654_619_320_123, 123_000, &TimeConfig::default()).unwrap(); /// assert_eq!(d.to_string(), "2022-06-07T16:28:40.246"); /// ``` - pub fn from_timestamp(timestamp: i64, timestamp_microsecond: u32) -> Result { + pub fn from_timestamp_with_config( + timestamp: i64, + timestamp_microsecond: u32, + config: &TimeConfig, + ) -> Result { let (mut second, extra_microsecond) = Date::timestamp_watershed(timestamp)?; let mut total_microsecond = timestamp_microsecond .checked_add(extra_microsecond) @@ -387,10 +392,48 @@ impl DateTime { let time_second = second.rem_euclid(86_400) as u32; Ok(Self { date, - time: Time::from_timestamp(time_second, total_microsecond)?, + time: Time::from_timestamp_with_config(time_second, total_microsecond, config)?, }) } + /// Create a datetime from a Unix Timestamp in seconds or milliseconds + /// + /// ("Unix Timestamp" means number of seconds or milliseconds since 1970-01-01) + /// + /// Input must be between `-11_676_096_000` (`1600-01-01T00:00:00`) and + /// `253_402_300_799_000` (`9999-12-31T23:59:59.999999`) inclusive. + /// + /// If the absolute value is > 2e10 (`20_000_000_000`) it is interpreted as being in milliseconds. + /// + /// That means: + /// * `20_000_000_000` is `2603-10-11T11:33:20` + /// * `20_000_000_001` is `1970-08-20T11:33:20.001` + /// * `-20_000_000_000` gives an error - `DateTooSmall` as it would be before 1600 + /// * `-20_000_000_001` is `1969-05-14T12:26:39.999` + /// + /// # Arguments + /// + /// * `timestamp` - timestamp in either seconds or milliseconds + /// * `timestamp_microsecond` - microseconds fraction of a second timestamp + /// + /// Where `timestamp` is interrupted as milliseconds and is not a whole second, the remainder is added to + /// `timestamp_microsecond`. + /// + /// # Examples + /// + /// ``` + /// use speedate::DateTime; + /// + /// let d = DateTime::from_timestamp(1_654_619_320, 123).unwrap(); + /// assert_eq!(d.to_string(), "2022-06-07T16:28:40.000123"); + /// + /// let d = DateTime::from_timestamp(1_654_619_320_123, 123_000).unwrap(); + /// assert_eq!(d.to_string(), "2022-06-07T16:28:40.246"); + /// ``` + pub fn from_timestamp(timestamp: i64, timestamp_microsecond: u32) -> Result { + Self::from_timestamp_with_config(timestamp, timestamp_microsecond, &TimeConfig::default()) + } + /// Create a datetime from the system time. This method uses [std::time::SystemTime] to get /// the system time and uses it to create a [DateTime] adjusted to the specified timezone offset. /// diff --git a/src/duration.rs b/src/duration.rs index 881222f..40bc237 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -231,7 +231,7 @@ impl Duration { /// ``` #[inline] pub fn parse_bytes(bytes: &[u8]) -> Result { - Duration::parse_bytes_with_config(bytes, TimeConfig::default()) + Duration::parse_bytes_with_config(bytes, &TimeConfig::default()) } /// Same as `Duration::parse_bytes` but with a TimeConfig component. @@ -246,7 +246,7 @@ impl Duration { /// ``` /// use speedate::{Duration, TimeConfig}; /// - /// let d = Duration::parse_bytes_with_config(b"P1Y", TimeConfig::default()).unwrap(); + /// let d = Duration::parse_bytes_with_config(b"P1Y", &TimeConfig::default()).unwrap(); /// assert_eq!( /// d, /// Duration { @@ -259,7 +259,7 @@ impl Duration { /// assert_eq!(d.to_string(), "P1Y"); /// ``` #[inline] - pub fn parse_bytes_with_config(bytes: &[u8], config: TimeConfig) -> Result { + pub fn parse_bytes_with_config(bytes: &[u8], config: &TimeConfig) -> Result { let (positive, offset) = match bytes.first().copied() { Some(b'+') => (true, 1), Some(b'-') => (false, 1), @@ -454,7 +454,7 @@ impl Duration { match bytes.get(position).copied() { Some(_) => { - let t = Time::parse_bytes_offset(bytes, position, TimeConfig::default())?; + let t = Time::parse_bytes_offset(bytes, position, &TimeConfig::default())?; Ok(Self { positive: false, // is set above @@ -467,7 +467,7 @@ impl Duration { } } - fn parse_time(bytes: &[u8], offset: usize, config: TimeConfig) -> Result { + fn parse_time(bytes: &[u8], offset: usize, config: &TimeConfig) -> Result { let t = crate::time::PureTime::parse(bytes, offset, config)?; if bytes.len() > t.position { diff --git a/src/lib.rs b/src/lib.rs index e2a981d..4dcb202 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -142,9 +142,11 @@ pub enum ParseError { TimeTooLarge, } +#[derive(Debug, Display, EnumMessage, PartialEq, Eq, Clone)] +#[strum(serialize_all = "snake_case")] pub enum ConfigError { // SecondsPrecisionOverflowBehavior string representation, must be one of "error" or "truncate" - UnknownSecondsPrecisionOverflowBehaviorString, + UnknownMicrosecondsPrecisionOverflowBehaviorString, } /// Used internally to write numbers to a buffer for `Display` of speedate types diff --git a/src/time.rs b/src/time.rs index 7d4bfa1..14cb9f0 100644 --- a/src/time.rs +++ b/src/time.rs @@ -186,7 +186,7 @@ impl Time { /// ``` #[inline] pub fn parse_bytes(bytes: &[u8]) -> Result { - Self::parse_bytes_offset(bytes, 0, TimeConfig::default()) + Self::parse_bytes_offset(bytes, 0, &TimeConfig::default()) } /// Same as `Time::parse_bytes` but with a `TimeConfig`. @@ -201,7 +201,7 @@ impl Time { /// ``` /// use speedate::{Time, TimeConfig}; /// - /// let d = Time::parse_bytes_with_config(b"12:13:14.123456", TimeConfig::default()).unwrap(); + /// let d = Time::parse_bytes_with_config(b"12:13:14.123456", &TimeConfig::default()).unwrap(); /// assert_eq!( /// d, /// Time { @@ -215,7 +215,7 @@ impl Time { /// assert_eq!(d.to_string(), "12:13:14.123456"); /// ``` #[inline] - pub fn parse_bytes_with_config(bytes: &[u8], config: TimeConfig) -> Result { + pub fn parse_bytes_with_config(bytes: &[u8], config: &TimeConfig) -> Result { Self::parse_bytes_offset(bytes, 0, config) } @@ -237,6 +237,32 @@ impl Time { /// assert_eq!(d.to_string(), "01:02:20.000123"); /// ``` pub fn from_timestamp(timestamp_second: u32, timestamp_microsecond: u32) -> Result { + Time::from_timestamp_with_config(timestamp_second, timestamp_microsecond, &TimeConfig::default()) + } + + /// Like `from_timestamp` but with a `TimeConfig` + /// + /// # Arguments + /// + /// * `timestamp_second` - timestamp in seconds + /// * `timestamp_microsecond` - microseconds fraction of a second timestamp + /// * `config` - the `TimeConfig` to use + /// + /// If `seconds + timestamp_microsecond` exceeds 86400, an error is returned. + /// + /// # Examples + /// + /// ``` + /// use speedate::{Time, TimeConfig}; + /// + /// let d = Time::from_timestamp_with_config(3740, 123, &TimeConfig::default()).unwrap(); + /// assert_eq!(d.to_string(), "01:02:20.000123"); + /// ``` + pub fn from_timestamp_with_config( + timestamp_second: u32, + timestamp_microsecond: u32, + config: &TimeConfig, + ) -> Result { let mut second = timestamp_second; let mut microsecond = timestamp_microsecond; if microsecond >= 1_000_000 { @@ -253,12 +279,12 @@ impl Time { minute: ((second % 3600) / 60) as u8, second: (second % 60) as u8, microsecond, - tz_offset: None, + tz_offset: config.unix_timestamp_offset, }) } /// Parse a time from bytes with a starting index, extra characters at the end of the string result in an error - pub(crate) fn parse_bytes_offset(bytes: &[u8], offset: usize, config: TimeConfig) -> Result { + pub(crate) fn parse_bytes_offset(bytes: &[u8], offset: usize, config: &TimeConfig) -> Result { let pure_time = PureTime::parse(bytes, offset, config)?; // Parse the timezone offset @@ -434,7 +460,7 @@ pub(crate) struct PureTime { } impl PureTime { - pub fn parse(bytes: &[u8], offset: usize, config: TimeConfig) -> Result { + pub fn parse(bytes: &[u8], offset: usize, config: &TimeConfig) -> Result { if bytes.len() - offset < 5 { return Err(ParseError::TooShort); } @@ -542,7 +568,7 @@ impl TryFrom<&str> for MicrosecondsPrecisionOverflowBehavior { match value.to_lowercase().as_str() { "truncate" => Ok(Self::Truncate), "error" => Ok(Self::Error), - _ => Err(ConfigError::UnknownSecondsPrecisionOverflowBehaviorString), + _ => Err(ConfigError::UnknownMicrosecondsPrecisionOverflowBehaviorString), } } } @@ -550,4 +576,5 @@ impl TryFrom<&str> for MicrosecondsPrecisionOverflowBehavior { #[derive(Debug, Clone, Default)] pub struct TimeConfig { pub microseconds_precision_overflow_behavior: MicrosecondsPrecisionOverflowBehavior, + pub unix_timestamp_offset: Option, } diff --git a/tests/main.rs b/tests/main.rs index bd75ef2..cfa1904 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -1250,8 +1250,9 @@ float_err_tests! { fn test_time_parse_truncate_seconds() { let time = Time::parse_bytes_with_config( "12:13:12.123456789".as_bytes(), - TimeConfig { + &TimeConfig { microseconds_precision_overflow_behavior: MicrosecondsPrecisionOverflowBehavior::Truncate, + ..Default::default() }, ) .unwrap(); @@ -1262,8 +1263,9 @@ fn test_time_parse_truncate_seconds() { fn test_datetime_parse_truncate_seconds() { let time = DateTime::parse_bytes_with_config( "2020-01-01T12:13:12.123456789".as_bytes(), - TimeConfig { + &TimeConfig { microseconds_precision_overflow_behavior: MicrosecondsPrecisionOverflowBehavior::Truncate, + ..Default::default() }, ) .unwrap(); @@ -1274,10 +1276,91 @@ fn test_datetime_parse_truncate_seconds() { fn test_duration_parse_truncate_seconds() { let time = Duration::parse_bytes_with_config( "00:00:00.1234567".as_bytes(), - TimeConfig { + &TimeConfig { microseconds_precision_overflow_behavior: MicrosecondsPrecisionOverflowBehavior::Truncate, + ..Default::default() }, ) .unwrap(); assert_eq!(time.to_string(), "PT0.123456S"); } + +#[test] +fn test_time_parse_bytes_does_not_add_offset_for_rfc3339() { + let time = Time::parse_bytes_with_config( + "12:13:12".as_bytes(), + &TimeConfig { + unix_timestamp_offset: Some(0), + ..Default::default() + }, + ) + .unwrap(); + assert_eq!(time.to_string(), "12:13:12"); +} + +#[test] +fn test_datetime_parse_bytes_does_not_add_offset_for_rfc3339() { + let time = DateTime::parse_bytes_with_config( + "2020-01-01T12:13:12".as_bytes(), + &TimeConfig { + unix_timestamp_offset: Some(0), + ..Default::default() + }, + ) + .unwrap(); + assert_eq!(time.to_string(), "2020-01-01T12:13:12"); +} + +#[test] +fn test_datetime_parse_unix_timestamp_from_bytes_with_utc_offset() { + let time = DateTime::parse_bytes_with_config( + "1689102037.5586429".as_bytes(), + &TimeConfig { + unix_timestamp_offset: Some(0), + ..Default::default() + }, + ) + .unwrap(); + assert_eq!(time.to_string(), "2023-07-11T19:00:37.558643Z"); +} + +#[test] +fn test_datetime_parse_unix_timestamp_from_bytes_as_naive() { + let time = DateTime::parse_bytes_with_config( + "1689102037.5586429".as_bytes(), + &TimeConfig { + unix_timestamp_offset: None, + ..Default::default() + }, + ) + .unwrap(); + assert_eq!(time.to_string(), "2023-07-11T19:00:37.558643"); +} + +#[test] +fn test_time_parse_unix_timestamp_from_bytes_with_utc_offset() { + let time = Time::from_timestamp_with_config( + 1, + 2, + &TimeConfig { + unix_timestamp_offset: Some(0), + ..Default::default() + }, + ) + .unwrap(); + assert_eq!(time.to_string(), "00:00:01.000002Z"); +} + +#[test] +fn test_time_parse_unix_timestamp_from_bytes_as_naive() { + let time = Time::from_timestamp_with_config( + 1, + 2, + &TimeConfig { + unix_timestamp_offset: None, + ..Default::default() + }, + ) + .unwrap(); + assert_eq!(time.to_string(), "00:00:01.000002"); +}