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, TimeZone, Timelike, Utc};
16use serde::{Deserialize, Deserializer, Serialize, Serializer};
17
18use crate::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        let ticks = self.checked_ticks();
42        ticks.serialize(serializer)
43    }
44}
45
46impl<'de> Deserialize<'de> for DateTime {
47    fn deserialize<D>(deserializer: D) -> Result<DateTime, D::Error>
48    where
49        D: Deserializer<'de>,
50    {
51        let ticks = i64::deserialize(deserializer)?;
52        Ok(DateTime::from(ticks))
53    }
54}
55
56/// DateTime encoded as 64-bit signed int
57impl BinaryEncoder<DateTime> for DateTime {
58    fn byte_len(&self) -> usize {
59        8
60    }
61
62    fn encode<S: Write>(&self, stream: &mut S) -> EncodingResult<usize> {
63        let ticks = self.checked_ticks();
64        write_i64(stream, ticks)
65    }
66
67    fn decode<S: Read>(stream: &mut S, decoding_options: &DecodingOptions) -> EncodingResult<Self> {
68        let ticks = read_i64(stream)?;
69        let date_time = DateTime::from(ticks);
70        // Client offset is a value that can be overridden to account for time discrepancies between client & server -
71        // note perhaps it is not a good idea to do it right here but it is the lowest point to intercept DateTime values.
72        Ok(date_time - decoding_options.client_offset)
73    }
74}
75
76impl Default for DateTime {
77    fn default() -> Self {
78        DateTime::epoch()
79    }
80}
81
82impl Add<Duration> for DateTime {
83    type Output = Self;
84
85    fn add(self, duration: Duration) -> Self {
86        DateTime::from(self.date_time + duration)
87    }
88}
89
90impl Sub<DateTime> for DateTime {
91    type Output = Duration;
92
93    fn sub(self, other: Self) -> Duration {
94        self.date_time - other.date_time
95    }
96}
97
98impl Sub<Duration> for DateTime {
99    type Output = Self;
100
101    fn sub(self, duration: Duration) -> Self {
102        DateTime::from(self.date_time - duration)
103    }
104}
105
106impl PartialOrd for DateTime {
107    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
108        Some(self.date_time.cmp(&other.date_time))
109    }
110}
111
112// From ymd_hms
113impl From<(u16, u16, u16, u16, u16, u16)> for DateTime {
114    fn from(dt: (u16, u16, u16, u16, u16, u16)) -> Self {
115        let (year, month, day, hour, minute, second) = dt;
116        DateTime::from((year, month, day, hour, minute, second, 0))
117    }
118}
119
120// From ymd_hms
121impl From<(u16, u16, u16, u16, u16, u16, u32)> for DateTime {
122    fn from(dt: (u16, u16, u16, u16, u16, u16, u32)) -> Self {
123        let (year, month, day, hour, minute, second, nanos) = dt;
124        if month < 1 || month > 12 {
125            panic!("Invalid month");
126        }
127        if day < 1 || day > 31 {
128            panic!("Invalid day");
129        }
130        if hour > 23 {
131            panic!("Invalid hour");
132        }
133        if minute > 59 {
134            panic!("Invalid minute");
135        }
136        if second > 59 {
137            panic!("Invalid second");
138        }
139        if nanos as i64 >= NANOS_PER_SECOND {
140            panic!("Invalid nanosecond");
141        }
142        let dt = Utc.ymd(year as i32, month as u32, day as u32).and_hms_nano(
143            hour as u32,
144            minute as u32,
145            second as u32,
146            nanos,
147        );
148        DateTime::from(dt)
149    }
150}
151
152impl From<DateTimeUtc> for DateTime {
153    fn from(date_time: DateTimeUtc) -> Self {
154        // OPC UA date time is more granular with nanos, so the value supplied is made granular too
155        let year = date_time.year();
156        let month = date_time.month();
157        let day = date_time.day();
158        let hour = date_time.hour();
159        let minute = date_time.minute();
160        let second = date_time.second();
161        let nanos = (date_time.nanosecond() / NANOS_PER_TICK as u32) * NANOS_PER_TICK as u32;
162        let date_time = Utc
163            .ymd(year, month, day)
164            .and_hms_nano(hour, minute, second, nanos);
165        DateTime { date_time }
166    }
167}
168
169impl From<i64> for DateTime {
170    fn from(value: i64) -> Self {
171        if value == i64::max_value() {
172            // Max signifies end times
173            Self::endtimes()
174        } else {
175            let secs = value / TICKS_PER_SECOND;
176            let nanos = (value - secs * TICKS_PER_SECOND) * NANOS_PER_TICK;
177            let duration = Duration::seconds(secs) + Duration::nanoseconds(nanos);
178            Self::from(Self::epoch_chrono() + duration)
179        }
180    }
181}
182
183impl Into<i64> for DateTime {
184    fn into(self) -> i64 {
185        self.checked_ticks()
186    }
187}
188
189impl Into<DateTimeUtc> for DateTime {
190    fn into(self) -> DateTimeUtc {
191        self.as_chrono()
192    }
193}
194
195impl fmt::Display for DateTime {
196    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
197        write!(f, "{}", self.date_time.to_rfc3339())
198    }
199}
200
201impl FromStr for DateTime {
202    type Err = ();
203
204    fn from_str(s: &str) -> Result<Self, Self::Err> {
205        DateTimeUtc::from_str(s).map(DateTime::from).map_err(|e| {
206            error!("Cannot parse date {}, error = {}", s, e);
207        })
208    }
209}
210
211impl DateTime {
212    /// Constructs from the current time
213    pub fn now() -> DateTime {
214        DateTime::from(Utc::now())
215    }
216
217    /// Constructs from the current time with an offset
218    pub fn now_with_offset(offset: Duration) -> DateTime {
219        DateTime::from(Utc::now() + offset)
220    }
221
222    /// Creates a null date time (i.e. the epoch)
223    pub fn null() -> DateTime {
224        // The epoch is 0, so effectively null
225        DateTime::epoch()
226    }
227
228    /// Tests if the date time is null (i.e. equal to epoch)
229    pub fn is_null(&self) -> bool {
230        self.ticks() == 0i64
231    }
232
233    /// Constructs a date time for the epoch
234    pub fn epoch() -> DateTime {
235        DateTime::from(Self::epoch_chrono())
236    }
237
238    /// Constructs a date time for the endtimes
239    pub fn endtimes() -> DateTime {
240        DateTime::from(Self::endtimes_chrono())
241    }
242
243    /// Returns the maximum tick value, corresponding to the end of time
244    pub fn endtimes_ticks() -> i64 {
245        Self::duration_to_ticks(Self::endtimes_chrono().signed_duration_since(Self::epoch_chrono()))
246    }
247
248    /// Constructs from a year, month, day
249    pub fn ymd(year: u16, month: u16, day: u16) -> DateTime {
250        DateTime::ymd_hms(year, month, day, 0, 0, 0)
251    }
252
253    /// Constructs from a year, month, day, hour, minute, second
254    pub fn ymd_hms(
255        year: u16,
256        month: u16,
257        day: u16,
258        hour: u16,
259        minute: u16,
260        second: u16,
261    ) -> DateTime {
262        DateTime::from((year, month, day, hour, minute, second))
263    }
264
265    /// Constructs from a year, month, day, hour, minute, second, nanosecond
266    pub fn ymd_hms_nano(
267        year: u16,
268        month: u16,
269        day: u16,
270        hour: u16,
271        minute: u16,
272        second: u16,
273        nanos: u32,
274    ) -> DateTime {
275        DateTime::from((year, month, day, hour, minute, second, nanos))
276    }
277
278    /// Returns the time in ticks, of 100 nanosecond intervals
279    pub fn ticks(&self) -> i64 {
280        Self::duration_to_ticks(self.date_time.signed_duration_since(Self::epoch_chrono()))
281    }
282
283    /// To checked ticks. Function returns 0 or MAX_INT64
284    /// if date exceeds valid OPC UA range
285    pub fn checked_ticks(&self) -> i64 {
286        let nanos = self.ticks();
287        if nanos < 0 {
288            return 0;
289        }
290        if nanos > Self::endtimes_ticks() {
291            return i64::max_value();
292        }
293        nanos
294    }
295
296    /// Time as chrono
297    pub fn as_chrono(&self) -> DateTimeUtc {
298        self.date_time
299    }
300
301    /// The OPC UA epoch - Jan 1 1601 00:00:00
302    fn epoch_chrono() -> DateTimeUtc {
303        Utc.ymd(MIN_YEAR as i32, 1, 1).and_hms(0, 0, 0)
304    }
305
306    /// The OPC UA endtimes - Dec 31 9999 23:59:59 i.e. the date after which dates are returned as MAX_INT64 ticks
307    /// Spec doesn't say what happens in the last second before midnight...
308    fn endtimes_chrono() -> DateTimeUtc {
309        Utc.ymd(MAX_YEAR as i32, 12, 31).and_hms(23, 59, 59)
310    }
311
312    /// Turns a duration to ticks
313    fn duration_to_ticks(duration: Duration) -> i64 {
314        // We can't directly ask for nanos because it will exceed i64,
315        // so we have to subtract the total seconds before asking for the nano portion
316        let seconds_part = Duration::seconds(duration.num_seconds());
317        let seconds = seconds_part.num_seconds();
318        let nanos = (duration - seconds_part).num_nanoseconds().unwrap();
319        // Put it back together in ticks
320        seconds * TICKS_PER_SECOND + nanos / NANOS_PER_TICK
321    }
322}