#![warn(clippy::missing_docs_in_private_items)]
mod display;
mod error;
pub use self::{
display::TimestampIso8601Display,
error::{TimestampParseError, TimestampParseErrorType},
};
use serde::{
de::{Deserialize, Deserializer, Error as DeError, Visitor},
ser::{Serialize, Serializer},
};
use std::{
fmt::{Formatter, Result as FmtResult},
str::FromStr,
};
use time::{OffsetDateTime, PrimitiveDateTime, format_description::well_known::Rfc3339};
const MICROSECONDS_PER_SECOND: i64 = 1_000_000;
const NANOSECONDS_PER_MICROSECOND: i64 = 1_000;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Timestamp(PrimitiveDateTime);
impl Timestamp {
pub fn from_micros(unix_microseconds: i64) -> Result<Self, TimestampParseError> {
let nanoseconds = i128::from(unix_microseconds) * i128::from(NANOSECONDS_PER_MICROSECOND);
OffsetDateTime::from_unix_timestamp_nanos(nanoseconds)
.map(|offset| Self(PrimitiveDateTime::new(offset.date(), offset.time())))
.map_err(TimestampParseError::from_component_range)
}
pub fn from_secs(unix_seconds: i64) -> Result<Self, TimestampParseError> {
OffsetDateTime::from_unix_timestamp(unix_seconds)
.map(|offset| Self(PrimitiveDateTime::new(offset.date(), offset.time())))
.map_err(TimestampParseError::from_component_range)
}
pub fn parse(datetime: &str) -> Result<Self, TimestampParseError> {
parse_iso8601(datetime).map(Self)
}
pub const fn as_secs(self) -> i64 {
self.0.assume_utc().unix_timestamp()
}
pub const fn as_micros(self) -> i64 {
let utc = self.0.assume_utc();
(utc.unix_timestamp() * MICROSECONDS_PER_SECOND) + (utc.microsecond() as i64)
}
pub const fn iso_8601(self) -> TimestampIso8601Display {
TimestampIso8601Display::new(self)
}
}
impl FromStr for Timestamp {
type Err = TimestampParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Timestamp::parse(s)
}
}
impl<'de> Deserialize<'de> for Timestamp {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct TimestampVisitor;
impl Visitor<'_> for TimestampVisitor {
type Value = Timestamp;
fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
f.write_str("iso 8601 datetime format")
}
fn visit_str<E: DeError>(self, v: &str) -> Result<Self::Value, E> {
Timestamp::parse(v).map_err(DeError::custom)
}
}
deserializer.deserialize_any(TimestampVisitor)
}
}
impl Serialize for Timestamp {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(&self.iso_8601())
}
}
impl TryFrom<&'_ str> for Timestamp {
type Error = TimestampParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::from_str(value)
}
}
fn parse_iso8601(input: &str) -> Result<PrimitiveDateTime, TimestampParseError> {
const TIMESTAMP_LENGTH: usize = "2021-01-01T01:01:01+00:00".len();
if input.len() < TIMESTAMP_LENGTH {
return Err(TimestampParseError::FORMAT);
}
OffsetDateTime::parse(input, &Rfc3339)
.map(|offset| PrimitiveDateTime::new(offset.date(), offset.time()))
.map_err(TimestampParseError::from_parse)
}
#[cfg(test)]
mod tests {
use super::{Timestamp, TimestampParseError};
use serde::{Deserialize, Serialize};
use static_assertions::assert_impl_all;
use std::{fmt::Debug, hash::Hash, str::FromStr};
use time::{OffsetDateTime, PrimitiveDateTime};
assert_impl_all!(
Timestamp: Clone,
Copy,
Debug,
Deserialize<'static>,
Eq,
FromStr,
Hash,
PartialEq,
Send,
Serialize,
Sync,
TryFrom<&'static str>,
);
#[test]
fn parse_iso8601() -> Result<(), TimestampParseError> {
let offset = OffsetDateTime::from_unix_timestamp_nanos(1_580_608_922_020_000_000).unwrap();
assert_eq!(
PrimitiveDateTime::new(offset.date(), offset.time()),
super::parse_iso8601("2020-02-02T02:02:02.020000+00:00")?
);
let offset = OffsetDateTime::from_unix_timestamp_nanos(1_580_608_922_000_000_000).unwrap();
assert_eq!(
PrimitiveDateTime::new(offset.date(), offset.time()),
super::parse_iso8601("2020-02-02T02:02:02+00:00")?
);
assert_eq!(
"2021-03-16T14:29:19.046000+00:00",
Timestamp::from_str("2021-03-16T14:29:19.046000+00:00")?
.iso_8601()
.to_string(),
);
assert_eq!(
"2022-03-16T14:29:19.046000+00:00",
Timestamp::from_str("2022-03-16T14:29:19.046000+00:00")?
.iso_8601()
.to_string(),
);
assert_eq!(
"2023-03-16T14:29:19.046000+00:00",
Timestamp::from_str("2023-03-16T14:29:19.046000+00:00")?
.iso_8601()
.to_string(),
);
Ok(())
}
#[test]
fn parse_iso8601_boundaries() -> Result<(), TimestampParseError> {
fn test(input: &str) -> Result<(), TimestampParseError> {
assert_eq!(input, Timestamp::from_str(input)?.iso_8601().to_string());
Ok(())
}
test("2021-12-31T23:59:59.999999+00:00")?;
test("2021-01-01T00:00:00.000000+00:00")?;
Ok(())
}
}