Skip to main content

c_its_parser/
time_utils.rs

1//! Conversions between ETSI and chrono values
2//!
3//! Take a look at the individual data types in [`crate::standards`] to discover available conversion methods and initialization functions.
4//!
5//! Note: These conversions are only available with the optional `time` feature flag.
6
7// used by IS 1.3.1
8
9#[cfg(any(
10    feature = "mapem_2_2_1",
11    feature = "spatem_2_2_1",
12    feature = "srem_2_2_1",
13    feature = "ssem_2_2_1",
14))]
15#[allow(
16    clippy::missing_panics_doc,
17    reason = "unwrap is safe b/c of preconditions"
18)]
19/// Converts a UTC time point to ETSI ASN.1 [`MinuteOfTheYear`](`crate::standards::dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear`)
20/// and [`DSecond`](`crate::standards::dsrc_2_2_1::etsi_its_dsrc::DSecond`) (milliseconds) values.
21#[must_use]
22pub fn moy_and_dsecond(
23    time: chrono::DateTime<chrono::Utc>,
24) -> (
25    crate::standards::dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear,
26    crate::standards::dsrc_2_2_1::etsi_its_dsrc::DSecond,
27) {
28    use chrono::{Datelike, Timelike};
29
30    // build start of year timestamp
31    let naive_time = time.naive_utc();
32    let start_of_year = chrono::NaiveDate::from_ymd_opt(naive_time.year(), 1, 1)
33        .expect("year of ref time suddenly out of range")
34        .and_time(chrono::NaiveTime::default());
35
36    // determine minute of the year and millis
37    let diff = time.naive_utc() - start_of_year;
38    #[allow(clippy::cast_possible_truncation, reason = "max of 527040 fits in u32")]
39    #[allow(clippy::cast_sign_loss, reason = "precondition assures positive value")]
40    let minutes = diff.num_minutes() as u32;
41
42    #[allow(clippy::cast_possible_truncation, reason = "max of 60000 fits in u16")]
43    let millis = (naive_time.second() * 1000 + naive_time.nanosecond() / 1_000_000) as u16;
44
45    let moy = crate::standards::dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear(minutes);
46    let dsec = crate::standards::dsrc_2_2_1::etsi_its_dsrc::DSecond::from_millis(millis)
47        .expect("DSecond suddenly out of range");
48    (moy, dsec)
49}
50
51#[cfg(any(
52    feature = "mapem_2_2_1",
53    feature = "spatem_2_2_1",
54    feature = "srem_2_2_1",
55    feature = "ssem_2_2_1",
56))]
57#[allow(
58    clippy::missing_panics_doc,
59    reason = "unwrap is safe b/c of preconditions"
60)]
61/// Converts ETSI ASN.1 [`MinuteOfTheYear`](`crate::standards::dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear`)
62/// and [`DSecond`](`crate::standards::dsrc_2_2_1::etsi_its_dsrc::DSecond`) (milliseconds) data to a UTC time point.
63#[must_use]
64pub fn time_from_moy_and_dsecond(
65    moy: &crate::standards::dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear,
66    second: &crate::standards::dsrc_2_2_1::etsi_its_dsrc::DSecond,
67    year: i32,
68) -> chrono::DateTime<chrono::Utc> {
69    // build start of year timestamp
70    let start_of_year = chrono::NaiveDate::from_ymd_opt(year, 1, 1)
71        .expect("year of ref time suddenly out of range")
72        .and_time(chrono::NaiveTime::default());
73
74    // add minutes of the year and milliseconds
75    let time = start_of_year
76        .checked_add_signed(chrono::TimeDelta::minutes(i64::from(moy.0)))
77        .expect("Resulting DateTime suddenly out of range")
78        .checked_add_signed(chrono::TimeDelta::milliseconds(i64::from(second.0)))
79        .expect("Resulting DateTime suddenly out of range");
80
81    time.and_utc()
82}
83
84/// convert between ETSI TimestampIts and [`chrono::DateTime`]
85///
86/// Note: UTC is 37 seconds behind TAI as of 2017-01-01 (when the last leap seconds was
87/// added to UTC).
88/// But the TimestampIts epoch starts at 2004-01-01 00:00:00 UTC which was 32 seconds
89/// behind TAI, so the difference is 5 seconds since the last leap second insertion on
90/// 2022-01-01.
91macro_rules! timestampits_conv_datetime {
92    ($t:ty) => {
93        impl From<$t> for chrono::DateTime<chrono::Utc> {
94            fn from(other: $t) -> Self {
95                const ITS_EPOCH_UNIX_MS: i64 = 1_072_915_200_000; // UNIX timestamp of ITS epoch begin
96
97                #[allow(clippy::cast_possible_wrap, reason = "42 bits fit in i64")]
98                let its_millis = other.0 as i64 + ITS_EPOCH_UNIX_MS;
99                // Note: This will use the wrong leap second count around the timestamp
100                //       where a leap second is introduced since we're comparing to UNIX
101                //       timestamps and the "corrected" timestamp. But this is irrelevant
102                //       for applications after 2022-01-01 and this was written in 2026.
103                let utc_millis = its_millis - i64::from(its_offset_ms(its_millis.cast_unsigned()));
104
105                chrono::DateTime::from_timestamp_millis(utc_millis)
106                    .expect("ITS Timestamp suddenly out of range for chrono::DateTime")
107            }
108        }
109
110        impl From<chrono::DateTime<chrono::Utc>> for $t {
111            fn from(other: chrono::DateTime<chrono::Utc>) -> $t {
112                const ITS_EPOCH_UNIX_MS: u64 = 1_072_915_200_000; // UNIX timestamp of ITS epoch begin
113
114                #[allow(
115                    clippy::cast_sign_loss,
116                    reason = "expecting positive UNIX time is fine"
117                )]
118                let utc_millis = other.timestamp_millis() as u64;
119                let its_time =
120                    utc_millis - ITS_EPOCH_UNIX_MS + u64::from(its_offset_ms(utc_millis));
121
122                Self(its_time)
123            }
124        }
125    };
126}
127
128fn its_offset_ms(unix_time_ms: u64) -> u16 {
129    if unix_time_ms >= 1_483_228_800_000 {
130        // leap second introduced at 2016-12-31, so +5 since 2017-01-01
131        5000
132    } else if unix_time_ms >= 1_435_708_800_000 {
133        // leap second introduced at 2015-06-30, so +4 since 2015-07-01
134        4000
135    } else if unix_time_ms >= 1_341_100_800_000 {
136        // leap second introduced at 2012-06-30, so +3 since 2012-07-01
137        3000
138    } else if unix_time_ms >= 1_199_145_600_000 {
139        // leap second introduced at 2008-12-31, so +2 since 2009-01-01
140        2000
141    } else if unix_time_ms >= 1_136_073_600_000 {
142        // leap second introduced at 2005-12-31, so +1 since 2006-01-01
143        1000
144    } else {
145        0
146    }
147}
148
149// used by DENM 1.3.1, IVIM 2.1.1
150#[cfg(any(feature = "denm_1_3_1", feature = "ivim_2_1_1"))]
151timestampits_conv_datetime!(crate::standards::cdd_1_3_1_1::its_container::TimestampIts);
152
153// used by CPM 2.1.1, DENM 2.2.1 and IVIM 2.2.1
154#[cfg(any(feature = "cpm_2_1_1", feature = "denm_2_2_1", feature = "ivim_2_2_1"))]
155timestampits_conv_datetime!(crate::standards::cdd_2_2_1::etsi_its_cdd::TimestampIts);
156
157// dummy data type to be used by anybody, without the need to pull in all ETSI data types
158pub struct TimestampIts(pub u64);
159timestampits_conv_datetime!(TimestampIts);
160
161// used by SPATEM 2.2.1
162#[cfg(feature = "_dsrc_2_2_1")]
163impl crate::standards::dsrc_2_2_1::etsi_its_dsrc::TimeMark {
164    /// Converts itself to an UTC date and time by using a reference time
165    ///
166    /// A reference time from the [`IntersectionState`](`crate::standards::dsrc_2_2_1::etsi_its_dsrc::IntersectionState`)'s `moy` value need to be supplied.
167    ///
168    /// ## Panics
169    /// May panic if dates suddenly exceed the value range
170    #[must_use]
171    pub fn to_datetime_from_moy(
172        &self,
173        moy: &crate::standards::dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear,
174        year: i32,
175    ) -> chrono::DateTime<chrono::Utc> {
176        // build minute of the year timestamp
177        let start_of_year = chrono::NaiveDate::from_ymd_opt(year, 1, 1)
178            .expect("year of ref time suddenly out of range")
179            .and_time(chrono::NaiveTime::default());
180        let ref_time = start_of_year
181            .checked_add_signed(chrono::TimeDelta::minutes(i64::from(moy.0)))
182            .expect("Resulting DateTime suddenly out of range")
183            .and_utc();
184
185        self.to_datetime_common(ref_time)
186    }
187
188    /// Converts itself to an UTC date and time by using a reference time
189    ///
190    /// The reference time can/ should be taken from the `moy` and `time_stamp` values of the [`IntersectionState`](`crate::standards::dsrc_2_2_1::etsi_its_dsrc::IntersectionState`).
191    ///
192    /// ## Panics
193    /// May panic if dates suddenly exceed the value range
194    #[must_use]
195    pub fn to_datetime_from_timestamp(
196        &self,
197        ref_time: &chrono::DateTime<chrono::Utc>,
198    ) -> chrono::DateTime<chrono::Utc> {
199        use chrono::Timelike;
200
201        // Round reference time down to full minutes
202        #[allow(
203            clippy::unwrap_used,
204            reason = "0 seconds and nanos are in the input range"
205        )]
206        let ref_time = ref_time
207            .with_second(0)
208            .and_then(|t| t.with_nanosecond(0))
209            .unwrap();
210
211        self.to_datetime_common(ref_time)
212    }
213
214    /// Converts itself to an UTC date and time from a reference time with minute-accuracy
215    ///
216    /// Important: `ref_time` **shall not** have seconds, millis or nanoseconds. It needs to have minute-accuracy!
217    fn to_datetime_common(
218        &self,
219        ref_time: chrono::DateTime<chrono::Utc>,
220    ) -> chrono::DateTime<chrono::Utc> {
221        use chrono::Timelike;
222
223        // If the value is out of range return one hour in the future
224        if self.is_out_of_range() {
225            return ref_time
226                .checked_add_signed(chrono::TimeDelta::hours(1))
227                .expect("Resulting DateTime suddenly out of range");
228        }
229
230        // add TimeMark to reference time
231        #[allow(clippy::unwrap_used, reason = "0 minutes is a valid input")]
232        let current_full_hour = ref_time.with_minute(0).unwrap();
233        let time_mark_time = current_full_hour
234            .checked_add_signed(chrono::TimeDelta::milliseconds(self.as_millis().into()))
235            .expect("Resulting DateTime suddenly out of range");
236
237        // add one hour, if timestamp seems to be in the past
238        // Note: C2C C2CCC_RS_2077_SPATMAP_AutomotiveRequirements.pdf and C-Roads state to just use minute-accuracy,
239        // but this is assured since we only used minutes to build our reference time (or rounded down to full minutes)
240        if time_mark_time < ref_time {
241            time_mark_time
242                .checked_add_signed(chrono::TimeDelta::hours(1))
243                .expect("Resulting DateTime suddenly out of range")
244        } else {
245            time_mark_time
246        }
247    }
248}
249// TimeMark
250
251#[cfg(all(test, feature = "_etsi"))]
252mod tests {
253
254    #[test]
255    #[cfg(any(
256        feature = "mapem_2_2_1",
257        feature = "spatem_2_2_1",
258        feature = "srem_2_2_1",
259        feature = "ssem_2_2_1",
260    ))]
261    fn time_to_moy_and_dsecond() {
262        use crate::time_utils::moy_and_dsecond;
263
264        // at 2026-01-01 00:00:00, moy shall be 0 and dsecond shall be 0
265        let date = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
266            .unwrap()
267            .and_time(chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())
268            .and_utc();
269        let (moy, dsec) = moy_and_dsecond(date);
270
271        assert_eq!(0, moy.0);
272        assert_eq!(0, dsec.0);
273
274        // at 2026-01-01 00:42:23, moy shall be 42 and dsecond shall be 23000 ms
275        let date = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
276            .unwrap()
277            .and_time(chrono::NaiveTime::from_hms_opt(0, 42, 23).unwrap())
278            .and_utc();
279        let (moy, dsec) = moy_and_dsecond(date);
280
281        assert_eq!(42, moy.0);
282        assert_eq!(23_000, dsec.0);
283
284        // at 2026-02-01 00:00:42, moy shall be (31*24*60) and dsecond shall be 42000 ms
285        let date = chrono::NaiveDate::from_ymd_opt(2026, 2, 1)
286            .unwrap()
287            .and_time(chrono::NaiveTime::from_hms_opt(0, 0, 42).unwrap())
288            .and_utc();
289        let (moy, dsec) = moy_and_dsecond(date);
290
291        assert_eq!(31 * 24 * 60, moy.0);
292        assert_eq!(42_000, dsec.0);
293    }
294
295    #[test]
296    #[cfg(any(
297        feature = "mapem_2_2_1",
298        feature = "spatem_2_2_1",
299        feature = "srem_2_2_1",
300        feature = "ssem_2_2_1",
301    ))]
302    fn moy_and_dsecond_to_time() {
303        use crate::standards::dsrc_2_2_1::etsi_its_dsrc::{DSecond, MinuteOfTheYear};
304        use crate::time_utils::time_from_moy_and_dsecond;
305
306        // year 2026, moy 0, dsecond 0 shall give 2026-01-01 00:00:00
307        let ref_date = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
308            .unwrap()
309            .and_time(chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())
310            .and_utc();
311
312        let date = time_from_moy_and_dsecond(&MinuteOfTheYear(0), &DSecond(0), 2026);
313        assert_eq!(ref_date, date);
314
315        // year 2026, moy 42 and dsecond 23000 ms shall give 2026-01-01 00:42:23
316        let ref_date = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
317            .unwrap()
318            .and_time(chrono::NaiveTime::from_hms_opt(0, 42, 23).unwrap())
319            .and_utc();
320
321        let date = time_from_moy_and_dsecond(&MinuteOfTheYear(42), &DSecond(23_000), 2026);
322        assert_eq!(ref_date, date);
323
324        // year 2024, moy (31*24*60) and dsecond 42000 ms shall give 2024-02-01 00:00:42,
325        let ref_date = chrono::NaiveDate::from_ymd_opt(2024, 2, 1)
326            .unwrap()
327            .and_time(chrono::NaiveTime::from_hms_opt(0, 0, 42).unwrap())
328            .and_utc();
329
330        let date =
331            time_from_moy_and_dsecond(&MinuteOfTheYear(31 * 24 * 60), &DSecond(42_000), 2024);
332        assert_eq!(ref_date, date);
333    }
334
335    #[test]
336    #[cfg(any(feature = "cpm_2_1_1", feature = "denm_2_2_1", feature = "ivim_2_2_1"))]
337    fn utc_to_its_timestamp() {
338        use crate::standards::cdd_2_2_1::etsi_its_cdd::TimestampIts;
339
340        // From ASN.1 definition: "The value for TimestampIts for 1 January 2007 00:00:00.000 UTC is `94 694 401 000` milliseconds"
341        let ref_date = chrono::NaiveDate::from_ymd_opt(2007, 1, 1)
342            .unwrap()
343            .and_time(chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())
344            .and_utc();
345
346        let its: TimestampIts = ref_date.into();
347        assert_eq!(94_694_401_000, its.0);
348    }
349
350    #[test]
351    #[cfg(any(feature = "cpm_2_1_1", feature = "denm_2_2_1", feature = "ivim_2_2_1"))]
352    fn its_to_utc_timestamp() {
353        use crate::standards::cdd_2_2_1::etsi_its_cdd::TimestampIts;
354
355        // From ASN.1 definition: "The value for TimestampIts for 1 January 2007 00:00:00.000 UTC is `94 694 401 000` milliseconds"
356        let ref_date = chrono::NaiveDate::from_ymd_opt(2007, 1, 1)
357            .unwrap()
358            .and_time(chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())
359            .and_utc();
360
361        let utc: chrono::DateTime<chrono::Utc> = TimestampIts(94_694_401_000).into();
362        assert_eq!(ref_date, utc);
363    }
364
365    #[test]
366    #[cfg(feature = "_dsrc_2_2_1")]
367    fn timemark_from_moy() {
368        use crate::standards::dsrc_2_2_1::etsi_its_dsrc::{MinuteOfTheYear, TimeMark};
369
370        // build 2026-01-01 12:10:00 UTC
371        let moy = MinuteOfTheYear(12 * 60 + 10);
372
373        // time mark: 12:10:15
374        let expected = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
375            .unwrap()
376            .and_time(chrono::NaiveTime::from_hms_opt(12, 10, 15).unwrap())
377            .and_utc();
378        let test_val_millis = (10 * 60 + 15) * 1000;
379        let time_mark = TimeMark::from_millis(test_val_millis).unwrap();
380
381        let res = time_mark.to_datetime_from_moy(&moy, 2026);
382        assert_eq!(expected, res);
383
384        // time mark: 12:09:55 -> 13:09:55
385        let expected = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
386            .unwrap()
387            .and_time(chrono::NaiveTime::from_hms_opt(13, 9, 55).unwrap())
388            .and_utc();
389        let test_val_millis = (9 * 60 + 55) * 1000;
390        let time_mark = TimeMark::from_millis(test_val_millis).unwrap();
391
392        let res = time_mark.to_datetime_from_moy(&moy, 2026);
393        assert_eq!(expected, res);
394
395        // time mark: 36000 -> one hour in future (13:10:00)
396        let expected = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
397            .unwrap()
398            .and_time(chrono::NaiveTime::from_hms_opt(13, 10, 00).unwrap())
399            .and_utc();
400        let time_mark = TimeMark::out_of_range();
401
402        let res = time_mark.to_datetime_from_moy(&moy, 2026);
403        assert_eq!(expected, res);
404    }
405
406    #[test]
407    #[cfg(feature = "_dsrc_2_2_1")]
408    fn timemark_from_time() {
409        use crate::standards::dsrc_2_2_1::etsi_its_dsrc::TimeMark;
410
411        // build 2026-01-01 12:10:15 UTC
412        let ref_time = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
413            .unwrap()
414            .and_time(chrono::NaiveTime::from_hms_opt(12, 10, 15).unwrap())
415            .and_utc();
416
417        // time mark: 12:10:15
418        let expected = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
419            .unwrap()
420            .and_time(chrono::NaiveTime::from_hms_opt(12, 10, 15).unwrap())
421            .and_utc();
422        let test_val_millis = (10 * 60 + 15) * 1000;
423        let time_mark = TimeMark::from_millis(test_val_millis).unwrap();
424
425        let res = time_mark.to_datetime_from_timestamp(&ref_time);
426        assert_eq!(expected, res);
427
428        // time mark: 12:09:55 -> 13:09:55
429        let expected = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
430            .unwrap()
431            .and_time(chrono::NaiveTime::from_hms_opt(13, 9, 55).unwrap())
432            .and_utc();
433        let test_val_millis = (9 * 60 + 55) * 1000;
434        let time_mark = TimeMark::from_millis(test_val_millis).unwrap();
435
436        let res = time_mark.to_datetime_from_timestamp(&ref_time);
437        assert_eq!(expected, res);
438
439        // time mark: 12:10:10 (should not be moved to next hour!)
440        let expected = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
441            .unwrap()
442            .and_time(chrono::NaiveTime::from_hms_opt(12, 10, 10).unwrap())
443            .and_utc();
444        let test_val_millis = (10 * 60 + 10) * 1000;
445        let time_mark = TimeMark::from_millis(test_val_millis).unwrap();
446
447        let res = time_mark.to_datetime_from_timestamp(&ref_time);
448        assert_eq!(expected, res);
449    }
450}