1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
use std::{
    fmt,
    ops::{Add, Sub},
};

use thiserror::Error;
use time::{
    format_description::FormatItem, macros::format_description, Duration, OffsetDateTime,
    PrimitiveDateTime,
};

/// A primitive unix timestamp without time zone.
///
/// We assume that all timestamps must be specified in the UTC time zone.

// This is a temporary workaround because
// [`time`](::time) does not allow to convert [`OffsetDateTime`] to [`PrimitiveDateTime`]
// (see <https://github.com/time-rs/time/pull/458>)
// and [`PrimitiveDateTime`] has e.g. no `unix_timestamp` method.
// So we internally use [`OffsetDateTime`] but the semantic is like [`PrimitiveDateTime`].
#[derive(Debug, Copy, PartialEq, Eq, Clone, PartialOrd, Ord)]
pub struct Timestamp(time::OffsetDateTime);

const TIMESTAMP_FORMAT: &[FormatItem] =
    format_description!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]");

impl fmt::Display for Timestamp {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        write!(f, "{}", self.0.format(TIMESTAMP_FORMAT).unwrap())
    }
}

#[derive(Debug, Error)]
#[error("Invalid time range: {0}")]
pub struct OutOfRangeError(String);

impl Timestamp {
    pub fn now() -> Self {
        Self(OffsetDateTime::now_utc())
    }

    pub fn try_from_secs(seconds: i64) -> Result<Self, OutOfRangeError> {
        let date_time = time::OffsetDateTime::from_unix_timestamp(seconds)
            .map_err(|err| OutOfRangeError(err.to_string()))?;
        Ok(Self(date_time))
    }

    #[deprecated]
    pub fn from_secs(seconds: i64) -> Self {
        Self::try_from_secs(seconds).unwrap()
    }

    pub fn try_from_millis(milliseconds: i64) -> Result<Self, OutOfRangeError> {
        let nanos = millis_to_nanos(milliseconds);
        let date_time = time::OffsetDateTime::from_unix_timestamp_nanos(nanos)
            .map_err(|err| OutOfRangeError(err.to_string()))?;
        Ok(Self(date_time))
    }

    #[deprecated]
    pub fn from_millis(milliseconds: i64) -> Self {
        Self::try_from_millis(milliseconds).unwrap()
    }

    pub fn as_secs(self) -> i64 {
        self.0.unix_timestamp()
    }

    pub fn as_millis(self) -> i64 {
        nanos_to_millis(self.0.unix_timestamp_nanos())
    }

    pub fn format(&self, fmt: &[FormatItem<'_>]) -> String {
        self.0.format(fmt).unwrap()
    }
    pub fn checked_sub(self, duration: Duration) -> Option<Self> {
        self.0.checked_sub(duration).map(Self)
    }
}

fn nanos_to_millis(nanos: i128) -> i64 {
    (nanos / 1_000_000).try_into().unwrap()
}

fn millis_to_nanos(millis: i64) -> i128 {
    i128::from(millis) * 1_000_000
}

impl From<PrimitiveDateTime> for Timestamp {
    fn from(ts: PrimitiveDateTime) -> Self {
        Self(ts.assume_utc())
    }
}

impl Add<Duration> for Timestamp {
    type Output = Self;
    fn add(self, d: time::Duration) -> Self {
        Self(self.0.add(d))
    }
}

impl Sub<Duration> for Timestamp {
    type Output = Self;
    fn sub(self, d: time::Duration) -> Self {
        Self(self.0.sub(d))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn format_timestamp() {
        let ts = Timestamp::try_from_millis(1_658_146_497_321).unwrap();
        assert_eq!("2022-07-18 12:14:57.321", format!("{ts}"));
    }
}