use std::fmt;
use std::time::{Duration, Instant, SystemTime};
use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
pub fn instant_to_system_time(instant: Instant) -> SystemTime {
SystemTime::now() - instant.elapsed()
}
pub fn duration_to_millis(duration: Duration) -> f64 {
(duration.as_secs_f64() * 1_000_000_000f64).round() / 1_000_000f64
}
pub fn chrono_to_positive_millis(duration: chrono::Duration) -> f64 {
duration_to_millis(duration.to_std().unwrap_or_default())
}
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct UnixTimestamp(u64);
impl UnixTimestamp {
pub const fn from_secs(secs: u64) -> Self {
Self(secs)
}
pub fn from_system(time: SystemTime) -> Self {
let duration = time
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self(duration)
}
pub fn from_datetime(date_time: DateTime<impl TimeZone>) -> Option<Self> {
let timestamp = date_time.timestamp();
if timestamp >= 0 {
Some(UnixTimestamp::from_secs(timestamp as u64))
} else {
None
}
}
#[inline]
pub fn from_instant(instant: Instant) -> Self {
Self::from_system(instant_to_system_time(instant))
}
#[inline]
pub fn now() -> Self {
Self::from_system(SystemTime::now())
}
pub fn as_secs(self) -> u64 {
self.0
}
pub fn as_datetime(self) -> Option<DateTime<Utc>> {
DateTime::from_timestamp(self.0 as i64, 0)
}
}
impl fmt::Debug for UnixTimestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "UnixTimestamp({})", self.as_secs())
}
}
impl fmt::Display for UnixTimestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_secs().fmt(f)
}
}
impl std::ops::Add<Duration> for UnixTimestamp {
type Output = Self;
fn add(self, rhs: Duration) -> Self::Output {
Self(self.0.saturating_add(rhs.as_secs()))
}
}
impl std::ops::Sub for UnixTimestamp {
type Output = Duration;
fn sub(self, rhs: Self) -> Self::Output {
Duration::from_secs(self.0 - rhs.0)
}
}
#[derive(Debug)]
pub struct ParseUnixTimestampError(());
impl std::str::FromStr for UnixTimestamp {
type Err = ParseUnixTimestampError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(datetime) = s.parse::<DateTime<Utc>>() {
let timestamp = datetime.timestamp();
if timestamp >= 0 {
return Ok(UnixTimestamp(timestamp as u64));
}
}
let ts = s.parse().or(Err(ParseUnixTimestampError(())))?;
Ok(Self(ts))
}
}
impl Serialize for UnixTimestamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u64(self.as_secs())
}
}
struct UnixTimestampVisitor;
impl<'de> serde::de::Visitor<'de> for UnixTimestampVisitor {
type Value = UnixTimestamp;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a non-negative timestamp or datetime string")
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(UnixTimestamp::from_secs(v))
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v < 0.0 || v > u64::MAX as f64 {
return Err(E::custom("timestamp out-of-range"));
}
Ok(UnixTimestamp::from_secs(v.trunc() as u64))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let datetime = v.parse::<DateTime<Utc>>().map_err(E::custom)?;
let timestamp = datetime.timestamp();
if timestamp >= 0 {
Ok(UnixTimestamp(timestamp as u64))
} else {
Err(E::custom("timestamp out-of-range"))
}
}
}
impl<'de> Deserialize<'de> for UnixTimestamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(UnixTimestampVisitor)
}
}
#[cfg(test)]
mod tests {
use serde_test::{assert_de_tokens, assert_de_tokens_error, assert_tokens, Token};
use super::*;
#[test]
fn test_parse_timestamp_int() {
assert_tokens(&UnixTimestamp::from_secs(123), &[Token::U64(123)]);
}
#[test]
fn test_parse_timestamp_neg_int() {
assert_de_tokens_error::<UnixTimestamp>(
&[Token::I64(-1)],
"invalid type: integer `-1`, expected a non-negative timestamp or datetime string",
);
}
#[test]
fn test_parse_timestamp_float() {
assert_de_tokens(&UnixTimestamp::from_secs(123), &[Token::F64(123.4)]);
}
#[test]
fn test_parse_timestamp_large_float() {
assert_de_tokens_error::<UnixTimestamp>(
&[Token::F64(2.0 * (u64::MAX as f64))],
"timestamp out-of-range",
);
}
#[test]
fn test_parse_timestamp_neg_float() {
assert_de_tokens_error::<UnixTimestamp>(&[Token::F64(-1.0)], "timestamp out-of-range");
}
#[test]
fn test_parse_timestamp_str() {
assert_de_tokens(
&UnixTimestamp::from_secs(123),
&[Token::Str("1970-01-01T00:02:03Z")],
);
}
#[test]
fn test_parse_timestamp_other() {
assert_de_tokens_error::<UnixTimestamp>(
&[Token::Bool(true)],
"invalid type: boolean `true`, expected a non-negative timestamp or datetime string",
);
}
#[test]
fn test_parse_datetime_bogus() {
assert_de_tokens_error::<UnixTimestamp>(
&[Token::Str("adf3rt546")],
"input contains invalid characters",
);
}
}