opcua_types/
date_time.rs

1// OPCUA for Rust
2// SPDX-License-Identifier: MPL-2.0
3// Copyright (C) 2017-2024 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::{Duration, SecondsFormat, TimeDelta, TimeZone, Timelike, Utc};
16use tracing::error;
17
18use crate::{encoding::*, Context};
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
27/// Alias for a chrono datetime at UTC, which is how OPC UA represent timestmaps.
28pub type DateTimeUtc = chrono::DateTime<Utc>;
29
30/// A date/time value. This is a wrapper around the chrono type with extra functionality
31/// for obtaining ticks in OPC UA measurements, endtimes, epoch etc.
32#[derive(PartialEq, Debug, Clone, Copy, Eq)]
33pub struct DateTime {
34    date_time: DateTimeUtc,
35}
36
37impl crate::UaNullable for DateTime {
38    fn is_ua_null(&self) -> bool {
39        self.is_null()
40    }
41}
42
43#[cfg(feature = "json")]
44mod json {
45    use crate::{json::*, Error};
46
47    use super::DateTime;
48
49    impl JsonEncodable for DateTime {
50        fn encode(
51            &self,
52            stream: &mut JsonStreamWriter<&mut dyn std::io::Write>,
53            _ctx: &crate::Context<'_>,
54        ) -> super::EncodingResult<()> {
55            Ok(stream.string_value(&self.to_rfc3339())?)
56        }
57    }
58
59    impl JsonDecodable for DateTime {
60        fn decode(
61            stream: &mut JsonStreamReader<&mut dyn std::io::Read>,
62            _ctx: &Context<'_>,
63        ) -> super::EncodingResult<Self> {
64            let v = stream.next_str()?;
65            let dt = DateTime::parse_from_rfc3339(v)
66                .map_err(|e| Error::decoding(format!("Cannot parse date time \"{v}\": {e}")))?;
67            Ok(dt)
68        }
69    }
70}
71
72#[cfg(feature = "xml")]
73mod xml {
74    use crate::xml::*;
75    use std::io::{Read, Write};
76
77    use super::DateTime;
78
79    impl XmlType for DateTime {
80        const TAG: &'static str = "DateTime";
81    }
82
83    impl XmlEncodable for DateTime {
84        fn encode(
85            &self,
86            writer: &mut XmlStreamWriter<&mut dyn Write>,
87            context: &Context<'_>,
88        ) -> EncodingResult<()> {
89            self.to_rfc3339().encode(writer, context)
90        }
91    }
92
93    impl XmlDecodable for DateTime {
94        fn decode(
95            read: &mut XmlStreamReader<&mut dyn Read>,
96            _context: &Context<'_>,
97        ) -> Result<Self, Error> {
98            let v = read.consume_as_text()?;
99            let dt = DateTime::parse_from_rfc3339(&v)
100                .map_err(|e| Error::decoding(format!("Cannot parse date time \"{v}\": {e}")))?;
101            Ok(dt)
102        }
103    }
104}
105
106/// DateTime encoded as 64-bit signed int
107impl BinaryEncodable for DateTime {
108    fn byte_len(&self, _ctx: &Context<'_>) -> usize {
109        8
110    }
111
112    fn encode<S: Write + ?Sized>(&self, stream: &mut S, _ctx: &Context<'_>) -> EncodingResult<()> {
113        let ticks = self.checked_ticks();
114        write_i64(stream, ticks)
115    }
116}
117
118impl BinaryDecodable for DateTime {
119    fn decode<S: Read + ?Sized>(stream: &mut S, ctx: &Context<'_>) -> EncodingResult<Self> {
120        let ticks = read_i64(stream)?;
121        let date_time = DateTime::from(ticks);
122        // Client offset is a value that can be overridden to account for time discrepancies between client & server -
123        // note perhaps it is not a good idea to do it right here but it is the lowest point to intercept DateTime values.
124        Ok(date_time - ctx.options().client_offset)
125    }
126}
127
128impl Default for DateTime {
129    fn default() -> Self {
130        DateTime::epoch()
131    }
132}
133
134impl Add<Duration> for DateTime {
135    type Output = Self;
136
137    fn add(self, duration: Duration) -> Self {
138        DateTime::from(self.date_time + duration)
139    }
140}
141
142impl Sub<DateTime> for DateTime {
143    type Output = Duration;
144
145    fn sub(self, other: Self) -> Duration {
146        self.date_time - other.date_time
147    }
148}
149
150impl Sub<Duration> for DateTime {
151    type Output = Self;
152
153    fn sub(self, duration: Duration) -> Self {
154        DateTime::from(self.date_time - duration)
155    }
156}
157
158impl PartialOrd for DateTime {
159    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
160        Some(self.cmp(other))
161    }
162}
163
164impl Ord for DateTime {
165    fn cmp(&self, other: &Self) -> Ordering {
166        self.date_time.cmp(&other.date_time)
167    }
168}
169
170// From ymd_hms
171impl From<(u16, u16, u16, u16, u16, u16)> for DateTime {
172    fn from(dt: (u16, u16, u16, u16, u16, u16)) -> Self {
173        let (year, month, day, hour, minute, second) = dt;
174        DateTime::from((year, month, day, hour, minute, second, 0))
175    }
176}
177
178// From ymd_hms
179impl From<(u16, u16, u16, u16, u16, u16, u32)> for DateTime {
180    fn from(dt: (u16, u16, u16, u16, u16, u16, u32)) -> Self {
181        let (year, month, day, hour, minute, second, nanos) = dt;
182        if !(1..=12).contains(&month) {
183            panic!("Invalid month");
184        }
185        if !(1..=31).contains(&day) {
186            panic!("Invalid day");
187        }
188        if hour > 23 {
189            panic!("Invalid hour");
190        }
191        if minute > 59 {
192            panic!("Invalid minute");
193        }
194        if second > 59 {
195            panic!("Invalid second");
196        }
197        if nanos as i64 >= NANOS_PER_SECOND {
198            panic!("Invalid nanosecond");
199        }
200        let dt = Utc
201            .with_ymd_and_hms(
202                year as i32,
203                month as u32,
204                day as u32,
205                hour as u32,
206                minute as u32,
207                second as u32,
208            )
209            .unwrap()
210            .with_nanosecond(nanos)
211            .unwrap();
212        DateTime::from(dt)
213    }
214}
215
216impl From<DateTimeUtc> for DateTime {
217    fn from(date_time: DateTimeUtc) -> Self {
218        // OPC UA date time is more granular with nanos, so the value supplied is made granular too
219        let nanos = (date_time.nanosecond() / NANOS_PER_TICK as u32) * NANOS_PER_TICK as u32;
220        let date_time = date_time.with_nanosecond(nanos).unwrap();
221        DateTime { date_time }
222    }
223}
224
225impl From<i64> for DateTime {
226    fn from(value: i64) -> Self {
227        if value == i64::MAX {
228            // Max signifies end times
229            Self::endtimes()
230        } else {
231            let secs = value / TICKS_PER_SECOND;
232            let nanos = (value - secs * TICKS_PER_SECOND) * NANOS_PER_TICK;
233            let duration = TimeDelta::try_seconds(secs).unwrap() + Duration::nanoseconds(nanos);
234            Self::from(Self::epoch_chrono() + duration)
235        }
236    }
237}
238
239impl From<DateTime> for i64 {
240    fn from(value: DateTime) -> Self {
241        value.checked_ticks()
242    }
243}
244
245impl From<DateTime> for DateTimeUtc {
246    fn from(value: DateTime) -> Self {
247        value.as_chrono()
248    }
249}
250
251impl fmt::Display for DateTime {
252    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
253        write!(f, "{}", self.date_time.to_rfc3339())
254    }
255}
256
257impl FromStr for DateTime {
258    type Err = chrono::ParseError;
259
260    fn from_str(s: &str) -> Result<Self, Self::Err> {
261        DateTimeUtc::from_str(s)
262            .map(DateTime::from)
263            .inspect_err(|e| {
264                error!("Cannot parse date {}, error = {}", s, e);
265            })
266    }
267}
268
269impl DateTime {
270    /// Constructs from the current time
271    pub fn now() -> DateTime {
272        DateTime::from(Utc::now())
273    }
274
275    /// For testing purposes only. This produces a version of now with no nanoseconds so it converts
276    /// in and out of rfc3999 without any loss of precision to make it easier to do comparison tests.
277    #[cfg(test)]
278    pub fn rfc3339_now() -> DateTime {
279        use std::time::{SystemTime, UNIX_EPOCH};
280
281        let duration = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
282        let now = DateTimeUtc::from_timestamp(duration.as_secs() as i64, 0).unwrap();
283        DateTime::from(now)
284    }
285
286    /// Constructs from the current time with an offset
287    pub fn now_with_offset(offset: Duration) -> DateTime {
288        DateTime::from(Utc::now() + offset)
289    }
290
291    /// Creates a null date time (i.e. the epoch)
292    pub fn null() -> DateTime {
293        // The epoch is 0, so effectively null
294        DateTime::epoch()
295    }
296
297    /// Tests if the date time is null (i.e. equal to epoch)
298    pub fn is_null(&self) -> bool {
299        self.ticks() == 0i64
300    }
301
302    /// Constructs a date time for the epoch
303    pub fn epoch() -> DateTime {
304        DateTime::from(Self::epoch_chrono())
305    }
306
307    /// Constructs a date time for the endtimes
308    pub fn endtimes() -> DateTime {
309        DateTime::from(Self::endtimes_chrono())
310    }
311
312    /// Returns the maximum tick value, corresponding to the end of time
313    pub fn endtimes_ticks() -> i64 {
314        Self::duration_to_ticks(Self::endtimes_chrono().signed_duration_since(Self::epoch_chrono()))
315    }
316
317    /// Constructs from a year, month, day
318    pub fn ymd(year: u16, month: u16, day: u16) -> DateTime {
319        DateTime::ymd_hms(year, month, day, 0, 0, 0)
320    }
321
322    /// Constructs from a year, month, day, hour, minute, second
323    pub fn ymd_hms(
324        year: u16,
325        month: u16,
326        day: u16,
327        hour: u16,
328        minute: u16,
329        second: u16,
330    ) -> DateTime {
331        DateTime::from((year, month, day, hour, minute, second))
332    }
333
334    /// Constructs from a year, month, day, hour, minute, second, nanosecond
335    pub fn ymd_hms_nano(
336        year: u16,
337        month: u16,
338        day: u16,
339        hour: u16,
340        minute: u16,
341        second: u16,
342        nanos: u32,
343    ) -> DateTime {
344        DateTime::from((year, month, day, hour, minute, second, nanos))
345    }
346
347    /// Returns an RFC 3339 and ISO 8601 date and time string such as 1996-12-19T16:39:57-08:00.
348    pub fn to_rfc3339(&self) -> String {
349        self.date_time.to_rfc3339_opts(SecondsFormat::Millis, true)
350    }
351
352    /// 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
353    pub fn parse_from_rfc3339(s: &str) -> Result<DateTime, chrono::ParseError> {
354        let date_time = chrono::DateTime::parse_from_rfc3339(s)?;
355        // Internally, the min date is going to get clipped to the epoch.
356        let mut date_time = date_time.with_timezone(&Utc);
357        if date_time < Self::epoch_chrono() {
358            date_time = Self::epoch_chrono();
359        }
360        // Clip to endtimes too
361        if date_time > Self::endtimes_chrono() {
362            date_time = Self::endtimes_chrono();
363        }
364
365        Ok(Self { date_time })
366    }
367
368    /// Returns the time in ticks, of 100 nanosecond intervals
369    pub fn ticks(&self) -> i64 {
370        Self::duration_to_ticks(self.date_time.signed_duration_since(Self::epoch_chrono()))
371    }
372
373    /// To checked ticks. Function returns 0 or MAX_INT64
374    /// if date exceeds valid OPC UA range
375    pub fn checked_ticks(&self) -> i64 {
376        let nanos = self.ticks();
377        if nanos < 0 {
378            return 0;
379        }
380        if nanos > Self::endtimes_ticks() {
381            return i64::MAX;
382        }
383        nanos
384    }
385
386    /// Time as chrono
387    pub fn as_chrono(&self) -> DateTimeUtc {
388        self.date_time
389    }
390
391    /// The OPC UA epoch - Jan 1 1601 00:00:00
392    fn epoch_chrono() -> DateTimeUtc {
393        Utc.with_ymd_and_hms(MIN_YEAR as i32, 1, 1, 0, 0, 0)
394            .unwrap()
395    }
396
397    /// The OPC UA endtimes - Dec 31 9999 23:59:59 i.e. the date after which dates are returned as MAX_INT64 ticks
398    /// Spec doesn't say what happens in the last second before midnight...
399    fn endtimes_chrono() -> DateTimeUtc {
400        Utc.with_ymd_and_hms(MAX_YEAR as i32, 12, 31, 23, 59, 59)
401            .unwrap()
402    }
403
404    /// Turns a duration to ticks
405    fn duration_to_ticks(duration: Duration) -> i64 {
406        // We can't directly ask for nanos because it will exceed i64,
407        // so we have to subtract the total seconds before asking for the nano portion
408        let seconds_part = TimeDelta::try_seconds(duration.num_seconds()).unwrap();
409        let seconds = seconds_part.num_seconds();
410        let nanos = (duration - seconds_part).num_nanoseconds().unwrap();
411        // Put it back together in ticks
412        seconds * TICKS_PER_SECOND + nanos / NANOS_PER_TICK
413    }
414}