Skip to content

Commit

Permalink
Add default_time_offset to TimeConfig (#45)
Browse files Browse the repository at this point in the history
* Add default_time_offset to TimeConfig

* fmt

* fix

* fix

* Handle timestamps

* Simplify / fix

* Remove unused UnknownTimestampDefaultOffsetString

* fix typo in test names

* remove unnecessary copy of config

---------

Co-authored-by: Samuel Colvin <s@muelcolvin.com>
  • Loading branch information
adriangb and samuelcolvin authored Jul 13, 2023
1 parent ed75c5b commit 7058870
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 31 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "speedate"
authors = ["Samuel Colvin <s@muelcolvin.com>"]
version = "0.10.0"
version = "0.11.0"
edition = "2021"
description = "Fast and simple datetime, date, time and duration parsing"
readme = "README.md"
Expand Down
71 changes: 57 additions & 14 deletions src/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ impl DateTime {
/// assert_eq!(dt.to_string(), "2022-01-01T12:13:14Z");
/// ```
pub fn parse_bytes_rfc3339(bytes: &[u8]) -> Result<Self, ParseError> {
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.
Expand All @@ -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 {
Expand All @@ -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<Self, ParseError> {
pub fn parse_bytes_rfc3339_with_config(bytes: &[u8], config: &TimeConfig) -> Result<Self, ParseError> {
// First up, parse the full date if we can
let date = Date::parse_bytes_partial(bytes)?;

Expand Down Expand Up @@ -305,7 +305,7 @@ impl DateTime {
/// assert_eq!(dt.to_string(), "2022-01-01T12:13:14");
/// ```
pub fn parse_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
DateTime::parse_bytes_with_config(bytes, TimeConfig::default())
DateTime::parse_bytes_with_config(bytes, &TimeConfig::default())
}

/// Same as `DateTime::parse_bytes` but supporting TimeConfig
Expand All @@ -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<Self, ParseError> {
pub fn parse_bytes_with_config(bytes: &[u8], config: &TimeConfig) -> Result<Self, ParseError> {
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)
///
Expand All @@ -356,22 +356,27 @@ 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`.
///
/// # 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<Self, ParseError> {
pub fn from_timestamp_with_config(
timestamp: i64,
timestamp_microsecond: u32,
config: &TimeConfig,
) -> Result<Self, ParseError> {
let (mut second, extra_microsecond) = Date::timestamp_watershed(timestamp)?;
let mut total_microsecond = timestamp_microsecond
.checked_add(extra_microsecond)
Expand All @@ -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, ParseError> {
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.
///
Expand Down
10 changes: 5 additions & 5 deletions src/duration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ impl Duration {
/// ```
#[inline]
pub fn parse_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
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.
Expand All @@ -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 {
Expand All @@ -259,7 +259,7 @@ impl Duration {
/// assert_eq!(d.to_string(), "P1Y");
/// ```
#[inline]
pub fn parse_bytes_with_config(bytes: &[u8], config: TimeConfig) -> Result<Self, ParseError> {
pub fn parse_bytes_with_config(bytes: &[u8], config: &TimeConfig) -> Result<Self, ParseError> {
let (positive, offset) = match bytes.first().copied() {
Some(b'+') => (true, 1),
Some(b'-') => (false, 1),
Expand Down Expand Up @@ -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
Expand All @@ -467,7 +467,7 @@ impl Duration {
}
}

fn parse_time(bytes: &[u8], offset: usize, config: TimeConfig) -> Result<Self, ParseError> {
fn parse_time(bytes: &[u8], offset: usize, config: &TimeConfig) -> Result<Self, ParseError> {
let t = crate::time::PureTime::parse(bytes, offset, config)?;

if bytes.len() > t.position {
Expand Down
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 34 additions & 7 deletions src/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ impl Time {
/// ```
#[inline]
pub fn parse_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
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`.
Expand All @@ -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 {
Expand All @@ -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<Self, ParseError> {
pub fn parse_bytes_with_config(bytes: &[u8], config: &TimeConfig) -> Result<Self, ParseError> {
Self::parse_bytes_offset(bytes, 0, config)
}

Expand All @@ -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<Self, ParseError> {
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<Self, ParseError> {
let mut second = timestamp_second;
let mut microsecond = timestamp_microsecond;
if microsecond >= 1_000_000 {
Expand All @@ -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<Self, ParseError> {
pub(crate) fn parse_bytes_offset(bytes: &[u8], offset: usize, config: &TimeConfig) -> Result<Self, ParseError> {
let pure_time = PureTime::parse(bytes, offset, config)?;

// Parse the timezone offset
Expand Down Expand Up @@ -434,7 +460,7 @@ pub(crate) struct PureTime {
}

impl PureTime {
pub fn parse(bytes: &[u8], offset: usize, config: TimeConfig) -> Result<Self, ParseError> {
pub fn parse(bytes: &[u8], offset: usize, config: &TimeConfig) -> Result<Self, ParseError> {
if bytes.len() - offset < 5 {
return Err(ParseError::TooShort);
}
Expand Down Expand Up @@ -542,12 +568,13 @@ 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),
}
}
}

#[derive(Debug, Clone, Default)]
pub struct TimeConfig {
pub microseconds_precision_overflow_behavior: MicrosecondsPrecisionOverflowBehavior,
pub unix_timestamp_offset: Option<i32>,
}
Loading

0 comments on commit 7058870

Please sign in to comment.