cbor_data/value/
timestamp.rs

1use crate::{constants::TAG_EPOCH, Encoder, ItemKind, Literal, TaggedItem};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
4pub enum Precision {
5    Seconds,
6    Millis,
7    Micros,
8    Nanos,
9}
10
11/// Representation of a Timestamp
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct Timestamp {
14    unix_epoch: i64,
15    nanos: u32,
16    tz_sec_east: i32,
17}
18
19impl Timestamp {
20    pub fn new(unix_epoch: i64, nanos: u32, tz_sec_east: i32) -> Self {
21        Self {
22            unix_epoch,
23            nanos,
24            tz_sec_east,
25        }
26    }
27
28    #[cfg(feature = "rfc3339")]
29    pub(crate) fn from_string(item: TaggedItem<'_>) -> Option<Self> {
30        if let ItemKind::Str(s) = item.kind() {
31            chrono::DateTime::parse_from_rfc3339(s.as_cow().as_ref())
32                .map(Into::into)
33                .ok()
34        } else {
35            None
36        }
37    }
38
39    pub(crate) fn from_epoch(item: TaggedItem<'_>) -> Option<Self> {
40        match item.kind() {
41            ItemKind::Pos(t) => Some(Timestamp {
42                unix_epoch: t.min(i64::MAX as u64) as i64,
43                nanos: 0,
44                tz_sec_east: 0,
45            }),
46            ItemKind::Neg(t) => Some(Timestamp {
47                unix_epoch: -1 - t.min(i64::MAX as u64) as i64,
48                nanos: 0,
49                tz_sec_east: 0,
50            }),
51            ItemKind::Float(t) => {
52                let mut frac = t.fract();
53                if frac < 0.0 {
54                    frac += 1.0;
55                }
56                let mut unix_epoch = t.min(i64::MAX as f64).floor() as i64;
57
58                let mut nanos = if t.abs() > 1e-3 * (1u64 << 52) as f64 {
59                    (frac * 1e3).round() as u32 * 1_000_000
60                } else if t.abs() > 1e-6 * (1u64 << 52) as f64 {
61                    (frac * 1e6).round() as u32 * 1_000
62                } else {
63                    (frac * 1e9).round() as u32
64                };
65                if nanos > 999_999_999 {
66                    nanos -= 1_000_000_000;
67                    unix_epoch += 1;
68                }
69
70                Some(Timestamp {
71                    unix_epoch,
72                    nanos,
73                    tz_sec_east: 0,
74                })
75            }
76            _ => None,
77        }
78    }
79
80    pub(crate) fn encode<E: Encoder>(self, encoder: E, precision: Precision) -> E::Output {
81        if precision == Precision::Seconds {
82            if self.unix_epoch() >= 0 {
83                encoder.write_pos(self.unix_epoch() as u64, [TAG_EPOCH])
84            } else {
85                encoder.write_neg((-1 - self.unix_epoch()) as u64, [TAG_EPOCH])
86            }
87        } else {
88            #[cfg(feature = "rfc3339")]
89            {
90                use crate::constants::TAG_ISO8601;
91                use chrono::{DateTime, FixedOffset};
92                use std::convert::TryFrom;
93
94                let mut this = self;
95                let as_epoch = match precision {
96                    Precision::Seconds => unreachable!(),
97                    Precision::Millis => {
98                        this.nanos -= this.nanos % 1_000_000;
99                        this.unix_epoch().abs() <= (1 << 52) / 1000
100                    }
101                    Precision::Micros => {
102                        this.nanos -= this.nanos % 1_000;
103                        this.unix_epoch().abs() <= (1 << 52) / 1_000_000
104                    }
105                    Precision::Nanos => this.unix_epoch().abs() <= (1 << 52) / 1_000_000_000,
106                };
107                if let (false, Ok(dt)) = (as_epoch, DateTime::<FixedOffset>::try_from(this)) {
108                    let s = dt.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true);
109                    encoder.write_str(s.as_str(), [TAG_ISO8601])
110                } else {
111                    let v = this.unix_epoch() as f64 + this.nanos() as f64 / 1e9;
112                    encoder.write_lit(Literal::L8(v.to_bits()), [TAG_EPOCH])
113                }
114            }
115            #[cfg(not(feature = "rfc3339"))]
116            {
117                let mut this = self;
118                match precision {
119                    Precision::Millis => this.nanos -= this.nanos % 1_000_000,
120                    Precision::Micros => this.nanos -= this.nanos % 1_000,
121                    _ => {}
122                };
123                let v = this.unix_epoch() as f64 + this.nanos() as f64 / 1e9;
124                encoder.write_lit(Literal::L8(v.to_bits()), [TAG_EPOCH])
125            }
126        }
127    }
128
129    /// timestamp value in seconds since the Unix epoch
130    pub fn unix_epoch(&self) -> i64 {
131        self.unix_epoch
132    }
133
134    /// fractional part in nanoseconds, to be added
135    pub fn nanos(&self) -> u32 {
136        self.nanos
137    }
138
139    /// timezone to use when encoding as a string, in seconds to the east
140    pub fn tz_sec_east(&self) -> i32 {
141        self.tz_sec_east
142    }
143}
144
145#[cfg(feature = "rfc3339")]
146mod rfc3339 {
147    use super::Timestamp;
148    use chrono::{DateTime, FixedOffset, Offset, TimeZone, Utc};
149    use std::convert::TryFrom;
150
151    impl TryFrom<Timestamp> for DateTime<FixedOffset> {
152        type Error = ();
153
154        fn try_from(t: Timestamp) -> Result<Self, Self::Error> {
155            Ok(FixedOffset::east_opt(t.tz_sec_east())
156                .ok_or(())?
157                .from_utc_datetime(
158                    &DateTime::<Utc>::from_timestamp(t.unix_epoch(), t.nanos())
159                        .ok_or(())?
160                        .naive_utc(),
161                ))
162        }
163    }
164
165    impl<Tz: TimeZone> From<DateTime<Tz>> for Timestamp {
166        fn from(dt: DateTime<Tz>) -> Self {
167            Timestamp {
168                unix_epoch: dt.timestamp(),
169                nanos: dt.timestamp_subsec_nanos(),
170                tz_sec_east: dt.offset().fix().local_minus_utc(),
171            }
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::{
179        Precision::{self, *},
180        Timestamp,
181    };
182    use crate::{constants::TAG_EPOCH, CborBuilder, Literal, Writer};
183
184    #[test]
185    fn encode() {
186        fn e(t: i64, n: u32, tz: i32, p: Precision) -> String {
187            let t = Timestamp {
188                unix_epoch: t,
189                nanos: n,
190                tz_sec_east: tz,
191            };
192            t.encode(CborBuilder::new(), p).to_string()
193        }
194
195        assert_eq!(e(0, 0, 0, Seconds), "1(0)");
196        assert_eq!(e(0, 900_000_000, 0, Seconds), "1(0)");
197        assert_eq!(e(-1, 0, 0, Seconds), "1(-1)");
198        assert_eq!(e(-1, 900_000_000, 0, Seconds), "1(-1)");
199        assert_eq!(e(i64::MAX, 0, 0, Seconds), "1(9223372036854775807)");
200        assert_eq!(e(i64::MIN, 0, 0, Seconds), "1(-9223372036854775808)");
201
202        assert_eq!(e(0, 500_000_000, 0, Millis), "1(0.5)");
203        #[cfg(feature = "rfc3339")]
204        {
205            assert_eq!(
206                e((1 << 52) / 1_000_000, 123_456_789, 0, Micros),
207                "1(4503599627.123456)"
208            );
209            assert_eq!(
210                e((1 << 52) / 1_000_000 + 1, 123_456_789, 2700, Micros),
211                "0(\"2112-09-18T00:38:48.123456+00:45\")"
212            );
213            assert_eq!(
214                e((1 << 52) / 1_000_000 + 1, 123_000_000, 2700, Micros),
215                "0(\"2112-09-18T00:38:48.123+00:45\")"
216            );
217            assert_eq!(
218                e((1 << 52) / 1_000_000_000, 123_456_789, 0, Nanos),
219                "1(4503599.123456789)"
220            );
221            assert_eq!(
222                e((1 << 52) / 1_000_000_000 + 1, 123_456_789, -1800, Nanos),
223                "0(\"1970-02-22T02:30:00.123456789-00:30\")"
224            );
225            assert_eq!(
226                e((1 << 52) / 1_000_000_000 + 1, 123_000_000, -1800, Nanos),
227                "0(\"1970-02-22T02:30:00.123-00:30\")"
228            );
229        }
230        #[cfg(not(feature = "rfc3339"))]
231        {
232            assert_eq!(
233                e((1 << 52) / 1_000_000, 123_456_789, 0, Micros),
234                "1(4503599627.123456)"
235            );
236            assert_eq!(
237                e((1 << 52) / 1_000_000 + 1, 123_456_789, 2700, Micros),
238                "1(4503599628.123456)"
239            );
240            assert_eq!(
241                e((1 << 52) / 1_000_000_000, 123_456_789, 0, Nanos),
242                "1(4503599.123456789)"
243            );
244            assert_eq!(
245                e((1 << 52) / 1_000_000_000 + 1, 123_456_789, -1800, Nanos),
246                "1(4503600.123456789)"
247            );
248        }
249    }
250
251    #[test]
252    #[cfg(feature = "rfc3339")]
253    fn string() {
254        use crate::{constants::TAG_ISO8601, Writer};
255
256        let cbor = CborBuilder::new().write_str("2020-07-12T02:14:00.43-04:40", [TAG_ISO8601]);
257        assert_eq!(
258            Timestamp::from_string(cbor.tagged_item()).unwrap(),
259            Timestamp::new(1594536840, 430_000_000, -16800)
260        );
261    }
262
263    #[test]
264    fn epoch() {
265        let cbor = CborBuilder::new().write_pos(1594536840, [TAG_EPOCH]);
266        assert_eq!(
267            Timestamp::from_epoch(cbor.tagged_item()).unwrap(),
268            Timestamp::new(1594536840, 0, 0)
269        );
270
271        let cbor = CborBuilder::new().write_neg(1594536840, [TAG_EPOCH]);
272        assert_eq!(
273            Timestamp::from_epoch(cbor.tagged_item()).unwrap(),
274            Timestamp::new(-1594536841, 0, 0)
275        );
276
277        let cbor =
278            CborBuilder::new().write_lit(Literal::L8(1594536840.01_f64.to_bits()), [TAG_EPOCH]);
279        assert_eq!(
280            Timestamp::from_epoch(cbor.tagged_item()).unwrap(),
281            Timestamp::new(1594536840, 9_999_990, 0) // f64 precision limit
282        );
283
284        let cbor =
285            CborBuilder::new().write_lit(Literal::L8(15945368400.01_f64.to_bits()), [TAG_EPOCH]);
286        assert_eq!(
287            Timestamp::from_epoch(cbor.tagged_item()).unwrap(),
288            Timestamp::new(15945368400, 10_000_000, 0) // ditching meaningless nanos here
289        );
290
291        let cbor =
292            CborBuilder::new().write_lit(Literal::L8((-15945368400.01_f64).to_bits()), [TAG_EPOCH]);
293        assert_eq!(
294            Timestamp::from_epoch(cbor.tagged_item()).unwrap(),
295            Timestamp::new(-15945368401, 990_000_000, 0)
296        );
297    }
298}