Skip to main content

opcua/types/
date_time.rs

1// OPCUA for Rust
2// SPDX-License-Identifier: MPL-2.0
3// Copyright (C) 2017-2022 Adam Lock
4
5//! Contains the implementation of `DataTime`.
6
7use std::{
8    cmp::Ordering,
9    fmt,
10    io::{Read, Write},
11    ops::{Add, Sub},
12    str::FromStr,
13};
14
15use chrono::{Datelike, Duration, SecondsFormat, TimeZone, Timelike, Utc};
16use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
17
18use crate::types::encoding::*;
19
20const NANOS_PER_SECOND: i64 = 1_000_000_000;
21const NANOS_PER_TICK: i64 = 100;
22const TICKS_PER_SECOND: i64 = NANOS_PER_SECOND / NANOS_PER_TICK;
23
24const MIN_YEAR: u16 = 1601;
25const MAX_YEAR: u16 = 9999;
26
27pub type DateTimeUtc = chrono::DateTime<Utc>;
28
29/// A date/time value. This is a wrapper around the chrono type with extra functionality
30/// for obtaining ticks in OPC UA measurements, endtimes, epoch etc.
31#[derive(PartialEq, Debug, Clone, Copy)]
32pub struct DateTime {
33    date_time: DateTimeUtc,
34}
35
36impl Serialize for DateTime {
37    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
38    where
39        S: Serializer,
40    {
41        serializer.serialize_str(&self.to_rfc3339())
42    }
43}
44
45impl<'de> Deserialize<'de> for DateTime {
46    fn deserialize<D>(deserializer: D) -> Result<DateTime, D::Error>
47    where
48        D: Deserializer<'de>,
49    {
50        let v = String::deserialize(deserializer)?;
51        let dt = DateTime::parse_from_rfc3339(&v)
52            .map_err(|_| D::Error::custom("Cannot parse date time"))?;
53        Ok(dt)
54    }
55}
56
57/// DateTime encoded as 64-bit signed int
58impl BinaryEncoder<DateTime> for DateTime {
59    fn byte_len(&self) -> usize {
60        8
61    }
62
63    fn encode<S: Write>(&self, stream: &mut S) -> EncodingResult<usize> {
64        let ticks = self.checked_ticks();
65        write_i64(stream, ticks)
66    }
67
68    fn decode<S: Read>(stream: &mut S, decoding_options: &DecodingOptions) -> EncodingResult<Self> {
69        let ticks = read_i64(stream)?;
70        let date_time = DateTime::from(ticks);
71        // Client offset is a value that can be overridden to account for time discrepancies between client & server -
72        // note perhaps it is not a good idea to do it right here but it is the lowest point to intercept DateTime values.
73        Ok(date_time - decoding_options.client_offset)
74    }
75}
76
77impl Default for DateTime {
78    fn default() -> Self {
79        DateTime::epoch()
80    }
81}
82
83impl Add<Duration> for DateTime {
84    type Output = Self;
85
86    fn add(self, duration: Duration) -> Self {
87        DateTime::from(self.date_time + duration)
88    }
89}
90
91impl Sub<DateTime> for DateTime {
92    type Output = Duration;
93
94    fn sub(self, other: Self) -> Duration {
95        self.date_time - other.date_time
96    }
97}
98
99impl Sub<Duration> for DateTime {
100    type Output = Self;
101
102    fn sub(self, duration: Duration) -> Self {
103        DateTime::from(self.date_time - duration)
104    }
105}
106
107impl PartialOrd for DateTime {
108    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
109        Some(self.date_time.cmp(&other.date_time))
110    }
111}
112
113// From ymd_hms
114impl From<(u16, u16, u16, u16, u16, u16)> for DateTime {
115    fn from(dt: (u16, u16, u16, u16, u16, u16)) -> Self {
116        let (year, month, day, hour, minute, second) = dt;
117        DateTime::from((year, month, day, hour, minute, second, 0))
118    }
119}
120
121// From ymd_hms
122impl From<(u16, u16, u16, u16, u16, u16, u32)> for DateTime {
123    fn from(dt: (u16, u16, u16, u16, u16, u16, u32)) -> Self {
124        let (year, month, day, hour, minute, second, nanos) = dt;
125        if month < 1 || month > 12 {
126            panic!("Invalid month");
127        }
128        if day < 1 || day > 31 {
129            panic!("Invalid day");
130        }
131        if hour > 23 {
132            panic!("Invalid hour");
133        }
134        if minute > 59 {
135            panic!("Invalid minute");
136        }
137        if second > 59 {
138            panic!("Invalid second");
139        }
140        if nanos as i64 >= NANOS_PER_SECOND {
141            panic!("Invalid nanosecond");
142        }
143        let dt = Utc.ymd(year as i32, month as u32, day as u32).and_hms_nano(
144            hour as u32,
145            minute as u32,
146            second as u32,
147            nanos,
148        );
149        DateTime::from(dt)
150    }
151}
152
153impl From<DateTimeUtc> for DateTime {
154    fn from(date_time: DateTimeUtc) -> Self {
155        // OPC UA date time is more granular with nanos, so the value supplied is made granular too
156        let year = date_time.year();
157        let month = date_time.month();
158        let day = date_time.day();
159        let hour = date_time.hour();
160        let minute = date_time.minute();
161        let second = date_time.second();
162        let nanos = (date_time.nanosecond() / NANOS_PER_TICK as u32) * NANOS_PER_TICK as u32;
163        let date_time = Utc
164            .ymd(year, month, day)
165            .and_hms_nano(hour, minute, second, nanos);
166        DateTime { date_time }
167    }
168}
169
170impl From<i64> for DateTime {
171    fn from(value: i64) -> Self {
172        if value == i64::max_value() {
173            // Max signifies end times
174            Self::endtimes()
175        } else {
176            let secs = value / TICKS_PER_SECOND;
177            let nanos = (value - secs * TICKS_PER_SECOND) * NANOS_PER_TICK;
178            let duration = Duration::seconds(secs) + Duration::nanoseconds(nanos);
179            Self::from(Self::epoch_chrono() + duration)
180        }
181    }
182}
183
184impl Into<i64> for DateTime {
185    fn into(self) -> i64 {
186        self.checked_ticks()
187    }
188}
189
190impl Into<DateTimeUtc> for DateTime {
191    fn into(self) -> DateTimeUtc {
192        self.as_chrono()
193    }
194}
195
196impl fmt::Display for DateTime {
197    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
198        write!(f, "{}", self.date_time.to_rfc3339())
199    }
200}
201
202impl FromStr for DateTime {
203    type Err = ();
204
205    fn from_str(s: &str) -> Result<Self, Self::Err> {
206        DateTimeUtc::from_str(s).map(DateTime::from).map_err(|e| {
207            error!("Cannot parse date {}, error = {}", s, e);
208        })
209    }
210}
211
212impl DateTime {
213    /// Constructs from the current time
214    pub fn now() -> DateTime {
215        DateTime::from(Utc::now())
216    }
217
218    /// For testing purposes only. This produces a version of now with no nanoseconds so it converts
219    /// in and out of rfc3999 without any loss of precision to make it easier to do comparison tests.
220    #[cfg(test)]
221    pub fn rfc3339_now() -> DateTime {
222        use chrono::NaiveDateTime;
223        use std::time::{SystemTime, UNIX_EPOCH};
224
225        let duration = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
226        let naive = NaiveDateTime::from_timestamp(duration.as_secs() as i64, 0);
227        let now = DateTimeUtc::from_utc(naive, Utc);
228        DateTime::from(now)
229    }
230
231    /// Constructs from the current time with an offset
232    pub fn now_with_offset(offset: Duration) -> DateTime {
233        DateTime::from(Utc::now() + offset)
234    }
235
236    /// Creates a null date time (i.e. the epoch)
237    pub fn null() -> DateTime {
238        // The epoch is 0, so effectively null
239        DateTime::epoch()
240    }
241
242    /// Tests if the date time is null (i.e. equal to epoch)
243    pub fn is_null(&self) -> bool {
244        self.ticks() == 0i64
245    }
246
247    /// Constructs a date time for the epoch
248    pub fn epoch() -> DateTime {
249        DateTime::from(Self::epoch_chrono())
250    }
251
252    /// Constructs a date time for the endtimes
253    pub fn endtimes() -> DateTime {
254        DateTime::from(Self::endtimes_chrono())
255    }
256
257    /// Returns the maximum tick value, corresponding to the end of time
258    pub fn endtimes_ticks() -> i64 {
259        Self::duration_to_ticks(Self::endtimes_chrono().signed_duration_since(Self::epoch_chrono()))
260    }
261
262    /// Constructs from a year, month, day
263    pub fn ymd(year: u16, month: u16, day: u16) -> DateTime {
264        DateTime::ymd_hms(year, month, day, 0, 0, 0)
265    }
266
267    /// Constructs from a year, month, day, hour, minute, second
268    pub fn ymd_hms(
269        year: u16,
270        month: u16,
271        day: u16,
272        hour: u16,
273        minute: u16,
274        second: u16,
275    ) -> DateTime {
276        DateTime::from((year, month, day, hour, minute, second))
277    }
278
279    /// Constructs from a year, month, day, hour, minute, second, nanosecond
280    pub fn ymd_hms_nano(
281        year: u16,
282        month: u16,
283        day: u16,
284        hour: u16,
285        minute: u16,
286        second: u16,
287        nanos: u32,
288    ) -> DateTime {
289        DateTime::from((year, month, day, hour, minute, second, nanos))
290    }
291
292    /// Returns an RFC 3339 and ISO 8601 date and time string such as 1996-12-19T16:39:57-08:00.
293    pub fn to_rfc3339(&self) -> String {
294        self.date_time.to_rfc3339_opts(SecondsFormat::Millis, true)
295    }
296
297    /// Parses an RFC 3339 and ISO 8601 date and time string such as 1996-12-19T16:39:57-08:00, then returns a new DateTime
298    pub fn parse_from_rfc3339(s: &str) -> Result<DateTime, ()> {
299        let date_time = chrono::DateTime::parse_from_rfc3339(s).map_err(|_| ())?;
300        // Internally, the min date is going to get clipped to the epoch.
301        let mut date_time = date_time.with_timezone(&Utc);
302        if date_time < Self::epoch_chrono() {
303            date_time = Self::epoch_chrono();
304        }
305        // Clip to endtimes too
306        if date_time > Self::endtimes_chrono() {
307            date_time = Self::endtimes_chrono();
308        }
309
310        Ok(Self { date_time })
311    }
312
313    /// Returns the time in ticks, of 100 nanosecond intervals
314    pub fn ticks(&self) -> i64 {
315        Self::duration_to_ticks(self.date_time.signed_duration_since(Self::epoch_chrono()))
316    }
317
318    /// To checked ticks. Function returns 0 or MAX_INT64
319    /// if date exceeds valid OPC UA range
320    pub fn checked_ticks(&self) -> i64 {
321        let nanos = self.ticks();
322        if nanos < 0 {
323            return 0;
324        }
325        if nanos > Self::endtimes_ticks() {
326            return i64::max_value();
327        }
328        nanos
329    }
330
331    /// Time as chrono
332    pub fn as_chrono(&self) -> DateTimeUtc {
333        self.date_time
334    }
335
336    /// The OPC UA epoch - Jan 1 1601 00:00:00
337    fn epoch_chrono() -> DateTimeUtc {
338        Utc.ymd(MIN_YEAR as i32, 1, 1).and_hms(0, 0, 0)
339    }
340
341    /// The OPC UA endtimes - Dec 31 9999 23:59:59 i.e. the date after which dates are returned as MAX_INT64 ticks
342    /// Spec doesn't say what happens in the last second before midnight...
343    fn endtimes_chrono() -> DateTimeUtc {
344        Utc.ymd(MAX_YEAR as i32, 12, 31).and_hms(23, 59, 59)
345    }
346
347    /// Turns a duration to ticks
348    fn duration_to_ticks(duration: Duration) -> i64 {
349        // We can't directly ask for nanos because it will exceed i64,
350        // so we have to subtract the total seconds before asking for the nano portion
351        let seconds_part = Duration::seconds(duration.num_seconds());
352        let seconds = seconds_part.num_seconds();
353        let nanos = (duration - seconds_part).num_nanoseconds().unwrap();
354        // Put it back together in ticks
355        seconds * TICKS_PER_SECOND + nanos / NANOS_PER_TICK
356    }
357}