w5500_sntp/
timestamp.rs

1/// SNTP timestamp format.
2///
3/// # References
4///
5/// * [RFC 4330 Section 3](https://datatracker.ietf.org/doc/html/rfc4330#section-3)
6#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
7#[cfg_attr(feature = "defmt", derive(defmt::Format))]
8pub struct Timestamp {
9    pub(crate) bits: u64,
10}
11
12impl Timestamp {
13    #[cfg(feature = "chrono")]
14    fn new(secs: u64, nanos: u64) -> Self {
15        let upper: u32 = secs
16            .try_into()
17            .unwrap_or_else(|_| secs.saturating_sub(1 << 32).try_into().unwrap_or(u32::MAX));
18        let lower: u32 = ((nanos * u64::from(u32::MAX)) / 1_000_000_000) as u32;
19        Self {
20            bits: ((upper as u64) << 32) | (lower as u64),
21        }
22    }
23
24    #[must_use]
25    #[cfg(any(feature = "chrono", feature = "time"))]
26    fn secs(&self) -> i64 {
27        let seconds_bits: u32 = (self.bits >> 32) as u32;
28        // If bit 0 is set, the UTC time is in the range 1968-2036
29        if seconds_bits & 0x8000_0000 != 0 {
30            i64::from(seconds_bits)
31        } else {
32            // If bit 0 is not set, the time is in the range 2036-2104 and
33            // UTC time is reckoned from 6h 28m 16s UTC on 7 February 2036.
34            i64::from(seconds_bits) + i64::from(u32::MAX) + 1
35        }
36    }
37
38    #[must_use]
39    #[cfg(any(feature = "chrono", feature = "time"))]
40    fn nanos(&self) -> u32 {
41        // safe to truncate, number is always less than u32::MAX
42        ((self.bits & 0xFFFF_FFFF) * 1_000_000_000 / u64::from(u32::MAX)) as u32
43    }
44
45    /// Raw bits of the timestamp value.
46    #[must_use]
47    pub const fn to_bits(self) -> u64 {
48        self.bits
49    }
50
51    /// Returns `true` if the timestamp is zero.
52    #[must_use]
53    pub const fn is_zero(&self) -> bool {
54        self.bits == 0
55    }
56}
57
58#[cfg(feature = "chrono")]
59fn origin_chrono() -> chrono::NaiveDateTime {
60    // TODO: constify when chrono 0.5.0 is available
61    unwrap!(unwrap!(chrono::NaiveDate::from_ymd_opt(1900, 1, 1)).and_hms_opt(0, 0, 0))
62}
63
64/// Returned upon a failed conversion to or from [`Timestamp`].
65#[derive(Debug, Copy, Clone, PartialEq, Eq)]
66#[cfg_attr(feature = "defmt", derive(defmt::Format))]
67pub struct TimestampError(pub(crate) ());
68
69#[cfg(feature = "chrono")]
70impl TryFrom<chrono::naive::NaiveDateTime> for Timestamp {
71    type Error = TimestampError;
72
73    fn try_from(ndt: chrono::naive::NaiveDateTime) -> Result<Self, Self::Error> {
74        let elapsed: chrono::TimeDelta = ndt.signed_duration_since(origin_chrono());
75        let secs: i64 = elapsed.num_seconds();
76        let secs_delta: chrono::TimeDelta =
77            chrono::TimeDelta::try_seconds(secs).ok_or(TimestampError(()))?;
78        let nanos: u64 = elapsed
79            .checked_sub(&secs_delta)
80            .unwrap_or_else(|| chrono::TimeDelta::try_seconds(0).unwrap())
81            .num_nanoseconds()
82            .unwrap_or(0)
83            .try_into()
84            .map_err(|_| TimestampError(()))?;
85        let secs: u64 = secs.try_into().map_err(|_| TimestampError(()))?;
86
87        Ok(Self::new(secs, nanos))
88    }
89}
90
91#[cfg(feature = "chrono")]
92impl TryFrom<Timestamp> for chrono::naive::NaiveDateTime {
93    type Error = TimestampError;
94
95    fn try_from(timestamp: Timestamp) -> Result<Self, Self::Error> {
96        let secs_delta: chrono::TimeDelta =
97            chrono::TimeDelta::try_seconds(timestamp.secs()).ok_or(TimestampError(()))?;
98        origin_chrono()
99            .checked_add_signed(secs_delta)
100            .ok_or(TimestampError(()))?
101            .checked_add_signed(chrono::TimeDelta::nanoseconds(timestamp.nanos().into()))
102            .ok_or(TimestampError(()))
103    }
104}
105
106#[cfg(feature = "time")]
107impl TryFrom<Timestamp> for time::PrimitiveDateTime {
108    type Error = TimestampError;
109
110    fn try_from(timestamp: Timestamp) -> Result<Self, Self::Error> {
111        const ORIGIN: time::PrimitiveDateTime = {
112            const DATE: time::Date =
113                match time::Date::from_calendar_date(1900, time::Month::January, 1) {
114                    Ok(date) => date,
115                    Err(_) => ::core::panic!("invalid date"),
116                };
117            const TIME: time::Time = match time::Time::from_hms(0, 0, 0) {
118                Ok(time) => time,
119                Err(_) => ::core::panic!("invalid time"),
120            };
121            time::PrimitiveDateTime::new(DATE, TIME)
122        };
123
124        ORIGIN
125            .checked_add(time::Duration::seconds(timestamp.secs()))
126            .ok_or(TimestampError(()))?
127            .checked_add(time::Duration::nanoseconds(timestamp.nanos().into()))
128            .ok_or(TimestampError(()))
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::Timestamp;
135    use chrono::naive::{NaiveDate, NaiveDateTime, NaiveTime};
136    use time::PrimitiveDateTime;
137
138    #[test]
139    fn chrono() {
140        let timestamp: Timestamp = Timestamp {
141            bits: 0xe5_fd_82_24_23_ec_4b_12,
142        };
143
144        let ndt: NaiveDateTime = timestamp.try_into().unwrap();
145
146        let expected_date: NaiveDate = NaiveDate::from_ymd_opt(2022, 4, 10).unwrap();
147        let expected_time: NaiveTime = NaiveTime::from_hms_nano_opt(16, 19, 48, 140324298).unwrap();
148        let expected_datetime: NaiveDateTime = NaiveDateTime::new(expected_date, expected_time);
149
150        core::assert_eq!(ndt, expected_datetime);
151
152        let timestamp_converted: Timestamp = ndt.try_into().unwrap();
153
154        core::assert_eq!(timestamp.secs(), timestamp_converted.secs());
155        let nanos_diff: u32 = timestamp.nanos().abs_diff(timestamp_converted.nanos());
156        core::assert!(nanos_diff <= 1, "nanos_diff={nanos_diff}");
157    }
158
159    #[test]
160    fn time() {
161        let timestamp: Timestamp = Timestamp {
162            bits: 0xe5_fd_82_24_23_ec_4b_12,
163        };
164
165        let pdt: PrimitiveDateTime = timestamp.try_into().unwrap();
166
167        core::assert_eq!(pdt.year(), 2022);
168        core::assert_eq!(pdt.month(), time::Month::April);
169        core::assert_eq!(pdt.day(), 10);
170        core::assert_eq!(pdt.hour(), 16);
171        core::assert_eq!(pdt.minute(), 19);
172        core::assert_eq!(pdt.second(), 48);
173    }
174
175    #[test]
176    fn chrono_zero() {
177        let timestamp: Timestamp = Timestamp { bits: 0 };
178
179        let ndt: NaiveDateTime = timestamp.try_into().unwrap();
180        let expected: NaiveDateTime = NaiveDate::from_ymd_opt(2036, 2, 7)
181            .unwrap()
182            .and_hms_opt(6, 28, 16)
183            .unwrap();
184
185        core::assert_eq!(ndt, expected);
186    }
187
188    #[test]
189    fn time_zero() {
190        let timestamp: Timestamp = Timestamp { bits: 0 };
191
192        let date: time::Date =
193            time::Date::from_calendar_date(2036, time::Month::February, 7).unwrap();
194        let time: time::Time = time::Time::from_hms(6, 28, 16).unwrap();
195        let expected: PrimitiveDateTime = PrimitiveDateTime::new(date, time);
196
197        let pdt: PrimitiveDateTime = timestamp.try_into().unwrap();
198
199        core::assert_eq!(pdt, expected);
200    }
201}