c-its-parser 2.2.0

Tools for encoding and decoding ETSI messages (GN + Transport + CAM/DENM/IVIM/SSEM/SREM/MAPEM/SPATEM)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
//! Conversions between ETSI and chrono values
//!
//! Take a look at the individual data types in [`crate::standards`] to discover available conversion methods and initialization functions.
//!
//! Note: These conversions are only available with the optional `time` feature flag.

// used by IS 1.3.1

#[cfg(any(
    feature = "mapem_2_2_1",
    feature = "spatem_2_2_1",
    feature = "srem_2_2_1",
    feature = "ssem_2_2_1",
))]
#[allow(
    clippy::missing_panics_doc,
    reason = "unwrap is safe b/c of preconditions"
)]
/// Converts a UTC time point to ETSI ASN.1 [`MinuteOfTheYear`](`crate::standards::dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear`)
/// and [`DSecond`](`crate::standards::dsrc_2_2_1::etsi_its_dsrc::DSecond`) (milliseconds) values.
#[must_use]
pub fn moy_and_dsecond(
    time: chrono::DateTime<chrono::Utc>,
) -> (
    crate::standards::dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear,
    crate::standards::dsrc_2_2_1::etsi_its_dsrc::DSecond,
) {
    use chrono::{Datelike, Timelike};

    // build start of year timestamp
    let naive_time = time.naive_utc();
    let start_of_year = chrono::NaiveDate::from_ymd_opt(naive_time.year(), 1, 1)
        .expect("year of ref time suddenly out of range")
        .and_time(chrono::NaiveTime::default());

    // determine minute of the year and millis
    let diff = time.naive_utc() - start_of_year;
    #[allow(clippy::cast_possible_truncation, reason = "max of 527040 fits in u32")]
    #[allow(clippy::cast_sign_loss, reason = "precondition assures positive value")]
    let minutes = diff.num_minutes() as u32;

    #[allow(clippy::cast_possible_truncation, reason = "max of 60000 fits in u16")]
    let millis = (naive_time.second() * 1000 + naive_time.nanosecond() / 1_000_000) as u16;

    let moy = crate::standards::dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear(minutes);
    let dsec = crate::standards::dsrc_2_2_1::etsi_its_dsrc::DSecond::from_millis(millis)
        .expect("DSecond suddenly out of range");
    (moy, dsec)
}

#[cfg(any(
    feature = "mapem_2_2_1",
    feature = "spatem_2_2_1",
    feature = "srem_2_2_1",
    feature = "ssem_2_2_1",
))]
#[allow(
    clippy::missing_panics_doc,
    reason = "unwrap is safe b/c of preconditions"
)]
/// Converts ETSI ASN.1 [`MinuteOfTheYear`](`crate::standards::dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear`)
/// and [`DSecond`](`crate::standards::dsrc_2_2_1::etsi_its_dsrc::DSecond`) (milliseconds) data to a UTC time point.
#[must_use]
pub fn time_from_moy_and_dsecond(
    moy: &crate::standards::dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear,
    second: &crate::standards::dsrc_2_2_1::etsi_its_dsrc::DSecond,
    year: i32,
) -> chrono::DateTime<chrono::Utc> {
    // build start of year timestamp
    let start_of_year = chrono::NaiveDate::from_ymd_opt(year, 1, 1)
        .expect("year of ref time suddenly out of range")
        .and_time(chrono::NaiveTime::default());

    // add minutes of the year and milliseconds
    let time = start_of_year
        .checked_add_signed(chrono::TimeDelta::minutes(i64::from(moy.0)))
        .expect("Resulting DateTime suddenly out of range")
        .checked_add_signed(chrono::TimeDelta::milliseconds(i64::from(second.0)))
        .expect("Resulting DateTime suddenly out of range");

    time.and_utc()
}

/// convert between ETSI TimestampIts and [`chrono::DateTime`]
///
/// Note: UTC is 37 seconds behind TAI as of 2017-01-01 (when the last leap seconds was
/// added to UTC).
/// But the TimestampIts epoch starts at 2004-01-01 00:00:00 UTC which was 32 seconds
/// behind TAI, so the difference is 5 seconds since the last leap second insertion on
/// 2022-01-01.
#[cfg(feature = "_etsi")]
macro_rules! timestampits_conv_datetime {
    ($t:ty) => {
        impl From<$t> for chrono::DateTime<chrono::Utc> {
            fn from(other: $t) -> Self {
                const ITS_EPOCH_UNIX_MS: i64 = 1_072_915_200_000; // UNIX timestamp of ITS epoch begin

                #[allow(clippy::cast_possible_wrap, reason = "42 bits fit in i64")]
                let its_millis = other.0 as i64 + ITS_EPOCH_UNIX_MS;
                // Note: This will use the wrong leap second count around the timestamp
                //       where a leap second is introduced since we're comparing to UNIX
                //       timestamps and the "corrected" timestamp. But this is irrelevant
                //       for applications after 2022-01-01 and this was written in 2026.
                let utc_millis = its_millis - i64::from(its_offset_ms(its_millis.cast_unsigned()));

                chrono::DateTime::from_timestamp_millis(utc_millis)
                    .expect("ITS Timestamp suddenly out of range for chrono::DateTime")
            }
        }

        impl From<chrono::DateTime<chrono::Utc>> for $t {
            fn from(other: chrono::DateTime<chrono::Utc>) -> $t {
                const ITS_EPOCH_UNIX_MS: u64 = 1_072_915_200_000; // UNIX timestamp of ITS epoch begin

                #[allow(
                    clippy::cast_sign_loss,
                    reason = "expecting positive UNIX time is fine"
                )]
                let utc_millis = other.timestamp_millis() as u64;
                let its_time =
                    utc_millis - ITS_EPOCH_UNIX_MS + u64::from(its_offset_ms(utc_millis));

                Self(its_time)
            }
        }
    };
}

#[cfg(feature = "_etsi")]
fn its_offset_ms(unix_time_ms: u64) -> u16 {
    if unix_time_ms >= 1_483_228_800_000 {
        // leap second introduced at 2016-12-31, so +5 since 2017-01-01
        5000
    } else if unix_time_ms >= 1_435_708_800_000 {
        // leap second introduced at 2015-06-30, so +4 since 2015-07-01
        4000
    } else if unix_time_ms >= 1_341_100_800_000 {
        // leap second introduced at 2012-06-30, so +3 since 2012-07-01
        3000
    } else if unix_time_ms >= 1_199_145_600_000 {
        // leap second introduced at 2008-12-31, so +2 since 2009-01-01
        2000
    } else if unix_time_ms >= 1_136_073_600_000 {
        // leap second introduced at 2005-12-31, so +1 since 2006-01-01
        1000
    } else {
        0
    }
}

// used by DENM 1.3.1, IVIM 2.1.1
#[cfg(any(feature = "denm_1_3_1", feature = "ivim_2_1_1"))]
timestampits_conv_datetime!(crate::standards::cdd_1_3_1_1::its_container::TimestampIts);

// used by CPM 2.1.1, DENM 2.2.1 and IVIM 2.2.1
#[cfg(any(feature = "cpm_2_1_1", feature = "denm_2_2_1", feature = "ivim_2_2_1"))]
timestampits_conv_datetime!(crate::standards::cdd_2_2_1::etsi_its_cdd::TimestampIts);

// used by SPATEM 2.2.1
#[cfg(feature = "_dsrc_2_2_1")]
impl crate::standards::dsrc_2_2_1::etsi_its_dsrc::TimeMark {
    /// Converts itself to an UTC date and time by using a reference time
    ///
    /// A reference time from the [`IntersectionState`](`crate::standards::dsrc_2_2_1::etsi_its_dsrc::IntersectionState`)'s `moy` value need to be supplied.
    ///
    /// ## Panics
    /// May panic if dates suddenly exceed the value range
    #[must_use]
    pub fn to_datetime_from_moy(
        &self,
        moy: &crate::standards::dsrc_2_2_1::etsi_its_dsrc::MinuteOfTheYear,
        year: i32,
    ) -> chrono::DateTime<chrono::Utc> {
        // build minute of the year timestamp
        let start_of_year = chrono::NaiveDate::from_ymd_opt(year, 1, 1)
            .expect("year of ref time suddenly out of range")
            .and_time(chrono::NaiveTime::default());
        let ref_time = start_of_year
            .checked_add_signed(chrono::TimeDelta::minutes(i64::from(moy.0)))
            .expect("Resulting DateTime suddenly out of range")
            .and_utc();

        self.to_datetime_common(ref_time)
    }

    /// Converts itself to an UTC date and time by using a reference time
    ///
    /// 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`).
    ///
    /// ## Panics
    /// May panic if dates suddenly exceed the value range
    #[must_use]
    pub fn to_datetime_from_timestamp(
        &self,
        ref_time: &chrono::DateTime<chrono::Utc>,
    ) -> chrono::DateTime<chrono::Utc> {
        use chrono::Timelike;

        // Round reference time down to full minutes
        #[allow(
            clippy::unwrap_used,
            reason = "0 seconds and nanos are in the input range"
        )]
        let ref_time = ref_time
            .with_second(0)
            .and_then(|t| t.with_nanosecond(0))
            .unwrap();

        self.to_datetime_common(ref_time)
    }

    /// Converts itself to an UTC date and time from a reference time with minute-accuracy
    ///
    /// Important: `ref_time` **shall not** have seconds, millis or nanoseconds. It needs to have minute-accuracy!
    fn to_datetime_common(
        &self,
        ref_time: chrono::DateTime<chrono::Utc>,
    ) -> chrono::DateTime<chrono::Utc> {
        use chrono::Timelike;

        // If the value is out of range return one hour in the future
        if self.is_out_of_range() {
            return ref_time
                .checked_add_signed(chrono::TimeDelta::hours(1))
                .expect("Resulting DateTime suddenly out of range");
        }

        // add TimeMark to reference time
        #[allow(clippy::unwrap_used, reason = "0 minutes is a valid input")]
        let current_full_hour = ref_time.with_minute(0).unwrap();
        let time_mark_time = current_full_hour
            .checked_add_signed(chrono::TimeDelta::milliseconds(self.as_millis().into()))
            .expect("Resulting DateTime suddenly out of range");

        // add one hour, if timestamp seems to be in the past
        // Note: C2C C2CCC_RS_2077_SPATMAP_AutomotiveRequirements.pdf and C-Roads state to just use minute-accuracy,
        // but this is assured since we only used minutes to build our reference time (or rounded down to full minutes)
        if time_mark_time < ref_time {
            time_mark_time
                .checked_add_signed(chrono::TimeDelta::hours(1))
                .expect("Resulting DateTime suddenly out of range")
        } else {
            time_mark_time
        }
    }
}
// TimeMark

#[cfg(all(test, feature = "_etsi"))]
mod tests {

    #[test]
    #[cfg(any(
        feature = "mapem_2_2_1",
        feature = "spatem_2_2_1",
        feature = "srem_2_2_1",
        feature = "ssem_2_2_1",
    ))]
    fn time_to_moy_and_dsecond() {
        use crate::time_utils::moy_and_dsecond;

        // at 2026-01-01 00:00:00, moy shall be 0 and dsecond shall be 0
        let date = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())
            .and_utc();
        let (moy, dsec) = moy_and_dsecond(date);

        assert_eq!(0, moy.0);
        assert_eq!(0, dsec.0);

        // at 2026-01-01 00:42:23, moy shall be 42 and dsecond shall be 23000 ms
        let date = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(0, 42, 23).unwrap())
            .and_utc();
        let (moy, dsec) = moy_and_dsecond(date);

        assert_eq!(42, moy.0);
        assert_eq!(23_000, dsec.0);

        // at 2026-02-01 00:00:42, moy shall be (31*24*60) and dsecond shall be 42000 ms
        let date = chrono::NaiveDate::from_ymd_opt(2026, 2, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(0, 0, 42).unwrap())
            .and_utc();
        let (moy, dsec) = moy_and_dsecond(date);

        assert_eq!(31 * 24 * 60, moy.0);
        assert_eq!(42_000, dsec.0);
    }

    #[test]
    #[cfg(any(
        feature = "mapem_2_2_1",
        feature = "spatem_2_2_1",
        feature = "srem_2_2_1",
        feature = "ssem_2_2_1",
    ))]
    fn moy_and_dsecond_to_time() {
        use crate::standards::dsrc_2_2_1::etsi_its_dsrc::{DSecond, MinuteOfTheYear};
        use crate::time_utils::time_from_moy_and_dsecond;

        // year 2026, moy 0, dsecond 0 shall give 2026-01-01 00:00:00
        let ref_date = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())
            .and_utc();

        let date = time_from_moy_and_dsecond(&MinuteOfTheYear(0), &DSecond(0), 2026);
        assert_eq!(ref_date, date);

        // year 2026, moy 42 and dsecond 23000 ms shall give 2026-01-01 00:42:23
        let ref_date = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(0, 42, 23).unwrap())
            .and_utc();

        let date = time_from_moy_and_dsecond(&MinuteOfTheYear(42), &DSecond(23_000), 2026);
        assert_eq!(ref_date, date);

        // year 2024, moy (31*24*60) and dsecond 42000 ms shall give 2024-02-01 00:00:42,
        let ref_date = chrono::NaiveDate::from_ymd_opt(2024, 2, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(0, 0, 42).unwrap())
            .and_utc();

        let date =
            time_from_moy_and_dsecond(&MinuteOfTheYear(31 * 24 * 60), &DSecond(42_000), 2024);
        assert_eq!(ref_date, date);
    }

    #[test]
    #[cfg(any(feature = "cpm_2_1_1", feature = "denm_2_2_1", feature = "ivim_2_2_1"))]
    fn utc_to_its_timestamp() {
        use crate::standards::cdd_2_2_1::etsi_its_cdd::TimestampIts;

        // From ASN.1 definition: "The value for TimestampIts for 1 January 2007 00:00:00.000 UTC is `94 694 401 000` milliseconds"
        let ref_date = chrono::NaiveDate::from_ymd_opt(2007, 1, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())
            .and_utc();

        let its: TimestampIts = ref_date.into();
        assert_eq!(94_694_401_000, its.0);
    }

    #[test]
    #[cfg(any(feature = "cpm_2_1_1", feature = "denm_2_2_1", feature = "ivim_2_2_1"))]
    fn its_to_utc_timestamp() {
        use crate::standards::cdd_2_2_1::etsi_its_cdd::TimestampIts;

        // From ASN.1 definition: "The value for TimestampIts for 1 January 2007 00:00:00.000 UTC is `94 694 401 000` milliseconds"
        let ref_date = chrono::NaiveDate::from_ymd_opt(2007, 1, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())
            .and_utc();

        let utc: chrono::DateTime<chrono::Utc> = TimestampIts(94_694_401_000).into();
        assert_eq!(ref_date, utc);
    }

    #[test]
    #[cfg(feature = "_dsrc_2_2_1")]
    fn timemark_from_moy() {
        use crate::standards::dsrc_2_2_1::etsi_its_dsrc::{MinuteOfTheYear, TimeMark};

        // build 2026-01-01 12:10:00 UTC
        let moy = MinuteOfTheYear(12 * 60 + 10);

        // time mark: 12:10:15
        let expected = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(12, 10, 15).unwrap())
            .and_utc();
        let test_val_millis = (10 * 60 + 15) * 1000;
        let time_mark = TimeMark::from_millis(test_val_millis).unwrap();

        let res = time_mark.to_datetime_from_moy(&moy, 2026);
        assert_eq!(expected, res);

        // time mark: 12:09:55 -> 13:09:55
        let expected = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(13, 9, 55).unwrap())
            .and_utc();
        let test_val_millis = (9 * 60 + 55) * 1000;
        let time_mark = TimeMark::from_millis(test_val_millis).unwrap();

        let res = time_mark.to_datetime_from_moy(&moy, 2026);
        assert_eq!(expected, res);

        // time mark: 36000 -> one hour in future (13:10:00)
        let expected = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(13, 10, 00).unwrap())
            .and_utc();
        let time_mark = TimeMark::out_of_range();

        let res = time_mark.to_datetime_from_moy(&moy, 2026);
        assert_eq!(expected, res);
    }

    #[test]
    #[cfg(feature = "_dsrc_2_2_1")]
    fn timemark_from_time() {
        use crate::standards::dsrc_2_2_1::etsi_its_dsrc::TimeMark;

        // build 2026-01-01 12:10:15 UTC
        let ref_time = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(12, 10, 15).unwrap())
            .and_utc();

        // time mark: 12:10:15
        let expected = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(12, 10, 15).unwrap())
            .and_utc();
        let test_val_millis = (10 * 60 + 15) * 1000;
        let time_mark = TimeMark::from_millis(test_val_millis).unwrap();

        let res = time_mark.to_datetime_from_timestamp(&ref_time);
        assert_eq!(expected, res);

        // time mark: 12:09:55 -> 13:09:55
        let expected = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(13, 9, 55).unwrap())
            .and_utc();
        let test_val_millis = (9 * 60 + 55) * 1000;
        let time_mark = TimeMark::from_millis(test_val_millis).unwrap();

        let res = time_mark.to_datetime_from_timestamp(&ref_time);
        assert_eq!(expected, res);

        // time mark: 12:10:10 (should not be moved to next hour!)
        let expected = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
            .unwrap()
            .and_time(chrono::NaiveTime::from_hms_opt(12, 10, 10).unwrap())
            .and_utc();
        let test_val_millis = (10 * 60 + 10) * 1000;
        let time_mark = TimeMark::from_millis(test_val_millis).unwrap();

        let res = time_mark.to_datetime_from_timestamp(&ref_time);
        assert_eq!(expected, res);
    }
}