dicom_core/value/
deserialize.rs

1//! Parsing of primitive values
2use crate::value::partial::{
3    check_component, DateComponent, DicomDate, DicomDateTime, DicomTime,
4    Error as PartialValuesError,
5};
6use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
7use snafu::{Backtrace, OptionExt, ResultExt, Snafu};
8use std::convert::TryFrom;
9use std::ops::{Add, Mul, Sub};
10
11#[derive(Debug, Snafu)]
12#[non_exhaustive]
13pub enum Error {
14    #[snafu(display("Unexpected end of element"))]
15    UnexpectedEndOfElement { backtrace: Backtrace },
16    #[snafu(display("Invalid date"))]
17    InvalidDate { backtrace: Backtrace },
18    #[snafu(display("Invalid time"))]
19    InvalidTime { backtrace: Backtrace },
20    #[snafu(display("Invalid DateTime"))]
21    InvalidDateTime {
22        #[snafu(backtrace)]
23        source: PartialValuesError,
24    },
25    #[snafu(display("Invalid date-time zone component"))]
26    InvalidDateTimeZone { backtrace: Backtrace },
27    #[snafu(display("Expected fraction delimiter '.', got '{}'", *value as char))]
28    FractionDelimiter { value: u8, backtrace: Backtrace },
29    #[snafu(display("Invalid number length: it is {}, but must be between 1 and 9", len))]
30    InvalidNumberLength { len: usize, backtrace: Backtrace },
31    #[snafu(display("Invalid number token: got '{}', but must be a digit in '0'..='9'", *value as char))]
32    InvalidNumberToken { value: u8, backtrace: Backtrace },
33    #[snafu(display("Invalid time zone sign token: got '{}', but must be '+' or '-'", *value as char))]
34    InvalidTimeZoneSignToken { value: u8, backtrace: Backtrace },
35    #[snafu(display(
36        "Could not parse incomplete value: first missing component: {:?}",
37        component
38    ))]
39    IncompleteValue {
40        component: DateComponent,
41        backtrace: Backtrace,
42    },
43    #[snafu(display("Component is invalid"))]
44    InvalidComponent {
45        #[snafu(backtrace)]
46        source: PartialValuesError,
47    },
48    #[snafu(display("Failed to construct partial value"))]
49    PartialValue {
50        #[snafu(backtrace)]
51        source: PartialValuesError,
52    },
53    #[snafu(display("Seconds '{secs}' out of bounds when constructing FixedOffset"))]
54    SecsOutOfBounds { secs: i32, backtrace: Backtrace },
55}
56
57type Result<T, E = Error> = std::result::Result<T, E>;
58
59/// Decode a single DICOM Date (DA) into a `chrono::NaiveDate` value.
60/// As per standard, a full 8 byte representation (YYYYMMDD) is required,
61/// otherwise, the operation fails.
62pub fn parse_date(buf: &[u8]) -> Result<NaiveDate> {
63    match buf.len() {
64        4 => IncompleteValueSnafu {
65            component: DateComponent::Month,
66        }
67        .fail(),
68        6 => IncompleteValueSnafu {
69            component: DateComponent::Day,
70        }
71        .fail(),
72        len if len >= 8 => {
73            let year = read_number(&buf[0..4])?;
74            let month: u32 = read_number(&buf[4..6])?;
75            check_component(DateComponent::Month, &month).context(InvalidComponentSnafu)?;
76
77            let day: u32 = read_number(&buf[6..8])?;
78            check_component(DateComponent::Day, &day).context(InvalidComponentSnafu)?;
79
80            NaiveDate::from_ymd_opt(year, month, day).context(InvalidDateSnafu)
81        }
82        _ => UnexpectedEndOfElementSnafu.fail(),
83    }
84}
85
86/** Decode a single DICOM Date (DA) into a `DicomDate` value.
87 * Unlike `parse_date`, this method accepts incomplete dates such as YYYY and YYYYMM
88 * The precision of the value is stored.
89 */
90pub fn parse_date_partial(buf: &[u8]) -> Result<(DicomDate, &[u8])> {
91    if buf.len() < 4 {
92        UnexpectedEndOfElementSnafu.fail()
93    } else {
94        let year: u16 = read_number(&buf[0..4])?;
95        let buf = &buf[4..];
96        if buf.len() < 2 {
97            Ok((DicomDate::from_y(year).context(PartialValueSnafu)?, buf))
98        } else {
99            match read_number::<u8>(&buf[0..2]) {
100                Err(_) => Ok((DicomDate::from_y(year).context(PartialValueSnafu)?, buf)),
101                Ok(month) => {
102                    let buf = &buf[2..];
103                    if buf.len() < 2 {
104                        Ok((
105                            DicomDate::from_ym(year, month).context(PartialValueSnafu)?,
106                            buf,
107                        ))
108                    } else {
109                        match read_number::<u8>(&buf[0..2]) {
110                            Err(_) => Ok((
111                                DicomDate::from_ym(year, month).context(PartialValueSnafu)?,
112                                buf,
113                            )),
114                            Ok(day) => {
115                                let buf = &buf[2..];
116                                Ok((
117                                    DicomDate::from_ymd(year, month, day)
118                                        .context(PartialValueSnafu)?,
119                                    buf,
120                                ))
121                            }
122                        }
123                    }
124                }
125            }
126        }
127    }
128}
129
130/** Decode a single DICOM Time (TM) into a `DicomTime` value.
131 * Unlike `parse_time`, this method allows for missing Time components.
132 * The precision of the second fraction is stored and can be returned as a range later.
133 */
134pub fn parse_time_partial(buf: &[u8]) -> Result<(DicomTime, &[u8])> {
135    if buf.len() < 2 {
136        UnexpectedEndOfElementSnafu.fail()
137    } else {
138        let hour: u8 = read_number(&buf[0..2])?;
139        let buf = &buf[2..];
140        if buf.len() < 2 {
141            Ok((DicomTime::from_h(hour).context(PartialValueSnafu)?, buf))
142        } else {
143            match read_number::<u8>(&buf[0..2]) {
144                Err(_) => Ok((DicomTime::from_h(hour).context(PartialValueSnafu)?, buf)),
145                Ok(minute) => {
146                    let buf = &buf[2..];
147                    if buf.len() < 2 {
148                        Ok((
149                            DicomTime::from_hm(hour, minute).context(PartialValueSnafu)?,
150                            buf,
151                        ))
152                    } else {
153                        match read_number::<u8>(&buf[0..2]) {
154                            Err(_) => Ok((
155                                DicomTime::from_hm(hour, minute).context(PartialValueSnafu)?,
156                                buf,
157                            )),
158                            Ok(second) => {
159                                let buf = &buf[2..];
160                                // buf contains at least ".F" otherwise ignore
161                                if buf.len() > 1 && buf[0] == b'.' {
162                                    let buf = &buf[1..];
163                                    let no_digits_index =
164                                        buf.iter().position(|b| !b.is_ascii_digit());
165                                    let max = no_digits_index.unwrap_or(buf.len());
166                                    let n = usize::min(6, max);
167                                    let fraction: u32 = read_number(&buf[0..n])?;
168                                    let buf = &buf[n..];
169                                    let fp = u8::try_from(n).unwrap();
170                                    Ok((
171                                        DicomTime::from_hmsf(hour, minute, second, fraction, fp)
172                                            .context(PartialValueSnafu)?,
173                                        buf,
174                                    ))
175                                } else {
176                                    Ok((
177                                        DicomTime::from_hms(hour, minute, second)
178                                            .context(PartialValueSnafu)?,
179                                        buf,
180                                    ))
181                                }
182                            }
183                        }
184                    }
185                }
186            }
187        }
188    }
189}
190
191/** Decode a single DICOM Time (TM) into a `chrono::NaiveTime` value.
192* If a time component is missing, the operation fails.
193* Presence of the second fraction component `.FFFFFF` is mandatory with at
194  least one digit accuracy `.F` while missing digits default to zero.
195* For Time with missing components, or if exact second fraction accuracy needs to be preserved,
196  use `parse_time_partial`.
197*/
198pub fn parse_time(buf: &[u8]) -> Result<(NaiveTime, &[u8])> {
199    // at least HHMMSS.F required
200    match buf.len() {
201        2 => IncompleteValueSnafu {
202            component: DateComponent::Minute,
203        }
204        .fail(),
205        4 => IncompleteValueSnafu {
206            component: DateComponent::Second,
207        }
208        .fail(),
209        6 => {
210            let hour: u32 = read_number(&buf[0..2])?;
211            check_component(DateComponent::Hour, &hour).context(InvalidComponentSnafu)?;
212            let minute: u32 = read_number(&buf[2..4])?;
213            check_component(DateComponent::Minute, &minute).context(InvalidComponentSnafu)?;
214            let second: u32 = read_number(&buf[4..6])?;
215            check_component(DateComponent::Second, &second).context(InvalidComponentSnafu)?;
216            Ok((
217                NaiveTime::from_hms_opt(hour, minute, second).context(InvalidTimeSnafu)?,
218                &buf[6..],
219            ))
220        }
221        len if len >= 8 => {
222            let hour: u32 = read_number(&buf[0..2])?;
223            check_component(DateComponent::Hour, &hour).context(InvalidComponentSnafu)?;
224            let minute: u32 = read_number(&buf[2..4])?;
225            check_component(DateComponent::Minute, &minute).context(InvalidComponentSnafu)?;
226            let second: u32 = read_number(&buf[4..6])?;
227            check_component(DateComponent::Second, &second).context(InvalidComponentSnafu)?;
228            let buf = &buf[6..];
229            if buf[0] != b'.' {
230                FractionDelimiterSnafu { value: buf[0] }.fail()
231            } else {
232                let buf = &buf[1..];
233                let no_digits_index = buf.iter().position(|b| !b.is_ascii_digit());
234                let max = no_digits_index.unwrap_or(buf.len());
235                let n = usize::min(6, max);
236                let mut fraction: u32 = read_number(&buf[0..n])?;
237                let mut acc = n;
238                while acc < 6 {
239                    fraction *= 10;
240                    acc += 1;
241                }
242                let buf = &buf[n..];
243                check_component(DateComponent::Fraction, &fraction)
244                    .context(InvalidComponentSnafu)?;
245                Ok((
246                    NaiveTime::from_hms_micro_opt(hour, minute, second, fraction)
247                        .context(InvalidTimeSnafu)?,
248                    buf,
249                ))
250            }
251        }
252        _ => UnexpectedEndOfElementSnafu.fail(),
253    }
254}
255
256/// A simple trait for types with a decimal form.
257pub trait Ten {
258    /// Retrieve the value ten. This returns `10` for integer types and
259    /// `10.` for floating point types.
260    fn ten() -> Self;
261}
262
263macro_rules! impl_integral_ten {
264    ($t:ty) => {
265        impl Ten for $t {
266            fn ten() -> Self {
267                10
268            }
269        }
270    };
271}
272
273macro_rules! impl_floating_ten {
274    ($t:ty) => {
275        impl Ten for $t {
276            fn ten() -> Self {
277                10.
278            }
279        }
280    };
281}
282
283impl_integral_ten!(i16);
284impl_integral_ten!(u16);
285impl_integral_ten!(u8);
286impl_integral_ten!(i32);
287impl_integral_ten!(u32);
288impl_integral_ten!(i64);
289impl_integral_ten!(u64);
290impl_integral_ten!(isize);
291impl_integral_ten!(usize);
292impl_floating_ten!(f32);
293impl_floating_ten!(f64);
294
295/// Retrieve an integer in text form.
296///
297/// All bytes in the text must be within the range b'0' and b'9'
298/// The text must also not be empty nor have more than 9 characters.
299pub fn read_number<T>(text: &[u8]) -> Result<T>
300where
301    T: Ten,
302    T: From<u8>,
303    T: Add<T, Output = T>,
304    T: Mul<T, Output = T>,
305    T: Sub<T, Output = T>,
306{
307    if text.is_empty() || text.len() > 9 {
308        return InvalidNumberLengthSnafu { len: text.len() }.fail();
309    }
310    if let Some(c) = text.iter().cloned().find(|b| !b.is_ascii_digit()) {
311        return InvalidNumberTokenSnafu { value: c }.fail();
312    }
313
314    Ok(read_number_unchecked(text))
315}
316
317#[inline]
318fn read_number_unchecked<T>(buf: &[u8]) -> T
319where
320    T: Ten,
321    T: From<u8>,
322    T: Add<T, Output = T>,
323    T: Mul<T, Output = T>,
324{
325    debug_assert!(!buf.is_empty());
326    debug_assert!(buf.len() < 10);
327    buf[1..].iter().fold((buf[0] - b'0').into(), |acc, v| {
328        acc * T::ten() + (*v - b'0').into()
329    })
330}
331
332/// Retrieve a `chrono::DateTime` from the given text, while assuming the given UTC offset.
333///
334/// If a date/time component is missing, the operation fails.
335/// Presence of the second fraction component `.FFFFFF` is mandatory with at
336/// least one digit accuracy `.F` while missing digits default to zero.
337///
338/// [`parse_datetime_partial`] should be preferred,
339/// because it is more flexible and resilient to missing components.
340/// See also the implementation of [`FromStr`](std::str::FromStr)
341/// for [`DicomDateTime`].
342#[deprecated(
343    since = "0.7.0",
344    note = "Use `parse_datetime_partial()` then `to_precise_datetime()`"
345)]
346pub fn parse_datetime(buf: &[u8], dt_utc_offset: FixedOffset) -> Result<DateTime<FixedOffset>> {
347    let date = parse_date(buf)?;
348    let buf = &buf[8..];
349    let (time, buf) = parse_time(buf)?;
350    let offset = match buf.len() {
351        0 => {
352            // A Date Time value without the optional suffix should be interpreted to be
353            // the local time zone of the application creating the Data Element, and can
354            // be overridden by the _Timezone Offset from UTC_ attribute.
355            let dt: Result<_> = dt_utc_offset
356                .from_local_datetime(&NaiveDateTime::new(date, time))
357                .single()
358                .context(InvalidDateTimeZoneSnafu);
359
360            return dt;
361        }
362        len if len > 4 => {
363            let tz_sign = buf[0];
364            let buf = &buf[1..];
365            let tz_h: i32 = read_number(&buf[0..2])?;
366            let tz_m: i32 = read_number(&buf[2..4])?;
367            let s = (tz_h * 60 + tz_m) * 60;
368            match tz_sign {
369                b'+' => FixedOffset::east_opt(s).context(SecsOutOfBoundsSnafu { secs: s })?,
370                b'-' => FixedOffset::west_opt(s).context(SecsOutOfBoundsSnafu { secs: s })?,
371                c => return InvalidTimeZoneSignTokenSnafu { value: c }.fail(),
372            }
373        }
374        _ => return UnexpectedEndOfElementSnafu.fail(),
375    };
376
377    offset
378        .from_local_datetime(&NaiveDateTime::new(date, time))
379        .single()
380        .context(InvalidDateTimeZoneSnafu)
381}
382
383/// Decode the text from the byte slice into a [`DicomDateTime`] value,
384/// which allows for missing Date / Time components.
385///
386/// This is the underlying implementation of [`FromStr`](std::str::FromStr)
387/// for `DicomDateTime`.
388///
389/// # Example
390///
391/// ```
392/// # use dicom_core::value::deserialize::parse_datetime_partial;
393/// use dicom_core::value::{DicomDate, DicomDateTime, DicomTime, PreciseDateTime};
394/// use chrono::Datelike;
395///
396/// let input = "20240201123456.000305";
397/// let dt = parse_datetime_partial(input.as_bytes())?;
398/// assert_eq!(
399///     dt,
400///     DicomDateTime::from_date_and_time(
401///         DicomDate::from_ymd(2024, 2, 1).unwrap(),
402///         DicomTime::from_hms_micro(12, 34, 56, 305).unwrap(),
403///     )?
404/// );
405/// // reinterpret as a chrono date time (with or without time zone)
406/// let dt: PreciseDateTime = dt.to_precise_datetime()?;
407/// // get just the date, for example
408/// let date = dt.to_naive_date();
409/// assert_eq!(date.year(), 2024);
410/// # Ok::<_, Box<dyn std::error::Error>>(())
411/// ```
412pub fn parse_datetime_partial(buf: &[u8]) -> Result<DicomDateTime> {
413    let (date, rest) = parse_date_partial(buf)?;
414
415    let (time, buf) = match parse_time_partial(rest) {
416        Ok((time, buf)) => (Some(time), buf),
417        Err(_) => (None, rest),
418    };
419
420    let time_zone = match buf.len() {
421        0 => None,
422        len if len > 4 => {
423            let tz_sign = buf[0];
424            let buf = &buf[1..];
425            let tz_h: u32 = read_number(&buf[0..2])?;
426            let tz_m: u32 = read_number(&buf[2..4])?;
427            let s = (tz_h * 60 + tz_m) * 60;
428            match tz_sign {
429                b'+' => {
430                    check_component(DateComponent::UtcEast, &s).context(InvalidComponentSnafu)?;
431                    Some(
432                        FixedOffset::east_opt(s as i32)
433                            .context(SecsOutOfBoundsSnafu { secs: s as i32 })?,
434                    )
435                }
436                b'-' => {
437                    check_component(DateComponent::UtcWest, &s).context(InvalidComponentSnafu)?;
438                    Some(
439                        FixedOffset::west_opt(s as i32)
440                            .context(SecsOutOfBoundsSnafu { secs: s as i32 })?,
441                    )
442                }
443                c => return InvalidTimeZoneSignTokenSnafu { value: c }.fail(),
444            }
445        }
446        _ => return UnexpectedEndOfElementSnafu.fail(),
447    };
448
449    match time_zone {
450        Some(time_zone) => match time {
451            Some(tm) => DicomDateTime::from_date_and_time_with_time_zone(date, tm, time_zone)
452                .context(InvalidDateTimeSnafu),
453            None => Ok(DicomDateTime::from_date_with_time_zone(date, time_zone)),
454        },
455        None => match time {
456            Some(tm) => DicomDateTime::from_date_and_time(date, tm).context(InvalidDateTimeSnafu),
457            None => Ok(DicomDateTime::from_date(date)),
458        },
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_parse_date() {
468        assert_eq!(
469            parse_date(b"20180101").unwrap(),
470            NaiveDate::from_ymd_opt(2018, 1, 1).unwrap()
471        );
472        assert_eq!(
473            parse_date(b"19711231").unwrap(),
474            NaiveDate::from_ymd_opt(1971, 12, 31).unwrap()
475        );
476        assert_eq!(
477            parse_date(b"20140426").unwrap(),
478            NaiveDate::from_ymd_opt(2014, 4, 26).unwrap()
479        );
480        assert_eq!(
481            parse_date(b"20180101xxxx").unwrap(),
482            NaiveDate::from_ymd_opt(2018, 1, 1).unwrap()
483        );
484        assert_eq!(
485            parse_date(b"19000101").unwrap(),
486            NaiveDate::from_ymd_opt(1900, 1, 1).unwrap()
487        );
488        assert_eq!(
489            parse_date(b"19620728").unwrap(),
490            NaiveDate::from_ymd_opt(1962, 7, 28).unwrap()
491        );
492        assert_eq!(
493            parse_date(b"19020404-0101").unwrap(),
494            NaiveDate::from_ymd_opt(1902, 4, 4).unwrap()
495        );
496
497        assert!(matches!(
498            parse_date(b"1902"),
499            Err(Error::IncompleteValue {
500                component: DateComponent::Month,
501                ..
502            })
503        ));
504
505        assert!(matches!(
506            parse_date(b"190208"),
507            Err(Error::IncompleteValue {
508                component: DateComponent::Day,
509                ..
510            })
511        ));
512
513        assert!(matches!(
514            parse_date(b"19021515"),
515            Err(Error::InvalidComponent {
516                source: PartialValuesError::InvalidComponent {
517                    component: DateComponent::Month,
518                    value: 15,
519                    ..
520                },
521                ..
522            })
523        ));
524
525        assert!(matches!(
526            parse_date(b"19021200"),
527            Err(Error::InvalidComponent {
528                source: PartialValuesError::InvalidComponent {
529                    component: DateComponent::Day,
530                    value: 0,
531                    ..
532                },
533                ..
534            })
535        ));
536
537        assert!(matches!(
538            parse_date(b"19021232"),
539            Err(Error::InvalidComponent {
540                source: PartialValuesError::InvalidComponent {
541                    component: DateComponent::Day,
542                    value: 32,
543                    ..
544                },
545                ..
546            })
547        ));
548
549        // not a leap year
550        assert!(matches!(
551            parse_date(b"20210229"),
552            Err(Error::InvalidDate { .. })
553        ));
554
555        assert!(parse_date(b"").is_err());
556        assert!(parse_date(b"        ").is_err());
557        assert!(parse_date(b"--------").is_err());
558        assert!(parse_date(&[0x00_u8; 8]).is_err());
559        assert!(parse_date(&[0xFF_u8; 8]).is_err());
560        assert!(parse_date(&[b'0'; 8]).is_err());
561        assert!(parse_date(b"nothing!").is_err());
562        assert!(parse_date(b"2012dec").is_err());
563    }
564
565    #[test]
566    fn test_parse_date_partial() {
567        assert_eq!(
568            parse_date_partial(b"20180101").unwrap(),
569            (DicomDate::from_ymd(2018, 1, 1).unwrap(), &[][..])
570        );
571        assert_eq!(
572            parse_date_partial(b"19711231").unwrap(),
573            (DicomDate::from_ymd(1971, 12, 31).unwrap(), &[][..])
574        );
575        assert_eq!(
576            parse_date_partial(b"20180101xxxx").unwrap(),
577            (DicomDate::from_ymd(2018, 1, 1).unwrap(), &b"xxxx"[..])
578        );
579        assert_eq!(
580            parse_date_partial(b"201801xxxx").unwrap(),
581            (DicomDate::from_ym(2018, 1).unwrap(), &b"xxxx"[..])
582        );
583        assert_eq!(
584            parse_date_partial(b"2018xxxx").unwrap(),
585            (DicomDate::from_y(2018).unwrap(), &b"xxxx"[..])
586        );
587        assert_eq!(
588            parse_date_partial(b"19020404-0101").unwrap(),
589            (DicomDate::from_ymd(1902, 4, 4).unwrap(), &b"-0101"[..][..])
590        );
591        assert_eq!(
592            parse_date_partial(b"201811").unwrap(),
593            (DicomDate::from_ym(2018, 11).unwrap(), &[][..])
594        );
595        assert_eq!(
596            parse_date_partial(b"1914").unwrap(),
597            (DicomDate::from_y(1914).unwrap(), &[][..])
598        );
599
600        assert_eq!(
601            parse_date_partial(b"19140").unwrap(),
602            (DicomDate::from_y(1914).unwrap(), &b"0"[..])
603        );
604
605        assert_eq!(
606            parse_date_partial(b"1914121").unwrap(),
607            (DicomDate::from_ym(1914, 12).unwrap(), &b"1"[..])
608        );
609
610        // does not check for leap year
611        assert_eq!(
612            parse_date_partial(b"20210229").unwrap(),
613            (DicomDate::from_ymd(2021, 2, 29).unwrap(), &[][..])
614        );
615
616        assert!(matches!(
617            parse_date_partial(b"19021515"),
618            Err(Error::PartialValue {
619                source: PartialValuesError::InvalidComponent {
620                    component: DateComponent::Month,
621                    value: 15,
622                    ..
623                },
624                ..
625            })
626        ));
627
628        assert!(matches!(
629            parse_date_partial(b"19021200"),
630            Err(Error::PartialValue {
631                source: PartialValuesError::InvalidComponent {
632                    component: DateComponent::Day,
633                    value: 0,
634                    ..
635                },
636                ..
637            })
638        ));
639
640        assert!(matches!(
641            parse_date_partial(b"19021232"),
642            Err(Error::PartialValue {
643                source: PartialValuesError::InvalidComponent {
644                    component: DateComponent::Day,
645                    value: 32,
646                    ..
647                },
648                ..
649            })
650        ));
651    }
652
653    #[test]
654    fn test_parse_time() {
655        assert_eq!(
656            parse_time(b"100000.1").unwrap(),
657            (
658                NaiveTime::from_hms_micro_opt(10, 0, 0, 100_000).unwrap(),
659                &[][..]
660            )
661        );
662        assert_eq!(
663            parse_time(b"235959.0123").unwrap(),
664            (
665                NaiveTime::from_hms_micro_opt(23, 59, 59, 12_300).unwrap(),
666                &[][..]
667            )
668        );
669        // only parses 6 digit precision as in DICOM standard
670        assert_eq!(
671            parse_time(b"235959.1234567").unwrap(),
672            (
673                NaiveTime::from_hms_micro_opt(23, 59, 59, 123_456).unwrap(),
674                &b"7"[..]
675            )
676        );
677        assert_eq!(
678            parse_time(b"235959.123456+0100").unwrap(),
679            (
680                NaiveTime::from_hms_micro_opt(23, 59, 59, 123_456).unwrap(),
681                &b"+0100"[..]
682            )
683        );
684        assert_eq!(
685            parse_time(b"235959.1-0100").unwrap(),
686            (
687                NaiveTime::from_hms_micro_opt(23, 59, 59, 100_000).unwrap(),
688                &b"-0100"[..]
689            )
690        );
691        assert_eq!(
692            parse_time(b"235959.12345+0100").unwrap(),
693            (
694                NaiveTime::from_hms_micro_opt(23, 59, 59, 123_450).unwrap(),
695                &b"+0100"[..]
696            )
697        );
698        assert_eq!(
699            parse_time(b"153011").unwrap(),
700            (NaiveTime::from_hms_opt(15, 30, 11).unwrap(), &b""[..])
701        );
702        assert_eq!(
703            parse_time(b"000000.000000").unwrap(),
704            (NaiveTime::from_hms_opt(0, 0, 0).unwrap(), &[][..])
705        );
706        assert!(matches!(
707            parse_time(b"23"),
708            Err(Error::IncompleteValue {
709                component: DateComponent::Minute,
710                ..
711            })
712        ));
713        assert!(matches!(
714            parse_time(b"1530"),
715            Err(Error::IncompleteValue {
716                component: DateComponent::Second,
717                ..
718            })
719        ));
720        assert!(matches!(
721            parse_time(b"153011x0110"),
722            Err(Error::FractionDelimiter { value: 0x78_u8, .. })
723        ));
724        assert!(parse_date(&[0x00_u8; 6]).is_err());
725        assert!(parse_date(&[0xFF_u8; 6]).is_err());
726        assert!(parse_date(b"075501.----").is_err());
727        assert!(parse_date(b"nope").is_err());
728        assert!(parse_date(b"235800.0a").is_err());
729    }
730    #[test]
731    fn test_parse_time_partial() {
732        assert_eq!(
733            parse_time_partial(b"10").unwrap(),
734            (DicomTime::from_h(10).unwrap(), &[][..])
735        );
736        assert_eq!(
737            parse_time_partial(b"101").unwrap(),
738            (DicomTime::from_h(10).unwrap(), &b"1"[..])
739        );
740        assert_eq!(
741            parse_time_partial(b"0755").unwrap(),
742            (DicomTime::from_hm(7, 55).unwrap(), &[][..])
743        );
744        assert_eq!(
745            parse_time_partial(b"075500").unwrap(),
746            (DicomTime::from_hms(7, 55, 0).unwrap(), &[][..])
747        );
748        assert_eq!(
749            parse_time_partial(b"065003").unwrap(),
750            (DicomTime::from_hms(6, 50, 3).unwrap(), &[][..])
751        );
752        assert_eq!(
753            parse_time_partial(b"075501.5").unwrap(),
754            (DicomTime::from_hmsf(7, 55, 1, 5, 1).unwrap(), &[][..])
755        );
756        assert_eq!(
757            parse_time_partial(b"075501.123").unwrap(),
758            (DicomTime::from_hmsf(7, 55, 1, 123, 3).unwrap(), &[][..])
759        );
760        assert_eq!(
761            parse_time_partial(b"10+0101").unwrap(),
762            (DicomTime::from_h(10).unwrap(), &b"+0101"[..])
763        );
764        assert_eq!(
765            parse_time_partial(b"1030+0101").unwrap(),
766            (DicomTime::from_hm(10, 30).unwrap(), &b"+0101"[..])
767        );
768        assert_eq!(
769            parse_time_partial(b"075501.123+0101").unwrap(),
770            (
771                DicomTime::from_hmsf(7, 55, 1, 123, 3).unwrap(),
772                &b"+0101"[..]
773            )
774        );
775        assert_eq!(
776            parse_time_partial(b"075501+0101").unwrap(),
777            (DicomTime::from_hms(7, 55, 1).unwrap(), &b"+0101"[..])
778        );
779        assert_eq!(
780            parse_time_partial(b"075501.999999").unwrap(),
781            (DicomTime::from_hmsf(7, 55, 1, 999_999, 6).unwrap(), &[][..])
782        );
783        assert_eq!(
784            parse_time_partial(b"075501.9999994").unwrap(),
785            (
786                DicomTime::from_hmsf(7, 55, 1, 999_999, 6).unwrap(),
787                &b"4"[..]
788            )
789        );
790        // 60 seconds for leap second
791        assert_eq!(
792            parse_time_partial(b"105960").unwrap(),
793            (DicomTime::from_hms(10, 59, 60).unwrap(), &[][..])
794        );
795        assert!(matches!(
796            parse_time_partial(b"24"),
797            Err(Error::PartialValue {
798                source: PartialValuesError::InvalidComponent {
799                    component: DateComponent::Hour,
800                    value: 24,
801                    ..
802                },
803                ..
804            })
805        ));
806        assert!(matches!(
807            parse_time_partial(b"1060"),
808            Err(Error::PartialValue {
809                source: PartialValuesError::InvalidComponent {
810                    component: DateComponent::Minute,
811                    value: 60,
812                    ..
813                },
814                ..
815            })
816        ));
817    }
818
819    #[test]
820    fn test_parse_datetime_partial() {
821        assert_eq!(
822            parse_datetime_partial(b"20171130101010.204").unwrap(),
823            DicomDateTime::from_date_and_time(
824                DicomDate::from_ymd(2017, 11, 30).unwrap(),
825                DicomTime::from_hmsf(10, 10, 10, 204, 3).unwrap(),
826            )
827            .unwrap()
828        );
829        assert_eq!(
830            parse_datetime_partial(b"20171130101010").unwrap(),
831            DicomDateTime::from_date_and_time(
832                DicomDate::from_ymd(2017, 11, 30).unwrap(),
833                DicomTime::from_hms(10, 10, 10).unwrap()
834            )
835            .unwrap()
836        );
837        assert_eq!(
838            parse_datetime_partial(b"2017113023").unwrap(),
839            DicomDateTime::from_date_and_time(
840                DicomDate::from_ymd(2017, 11, 30).unwrap(),
841                DicomTime::from_h(23).unwrap()
842            )
843            .unwrap()
844        );
845        assert_eq!(
846            parse_datetime_partial(b"201711").unwrap(),
847            DicomDateTime::from_date(DicomDate::from_ym(2017, 11).unwrap())
848        );
849        assert_eq!(
850            parse_datetime_partial(b"20171130101010.204+0535").unwrap(),
851            DicomDateTime::from_date_and_time_with_time_zone(
852                DicomDate::from_ymd(2017, 11, 30).unwrap(),
853                DicomTime::from_hmsf(10, 10, 10, 204, 3).unwrap(),
854                FixedOffset::east_opt(5 * 3600 + 35 * 60).unwrap()
855            )
856            .unwrap()
857        );
858        assert_eq!(
859            parse_datetime_partial(b"20171130101010+0535").unwrap(),
860            DicomDateTime::from_date_and_time_with_time_zone(
861                DicomDate::from_ymd(2017, 11, 30).unwrap(),
862                DicomTime::from_hms(10, 10, 10).unwrap(),
863                FixedOffset::east_opt(5 * 3600 + 35 * 60).unwrap()
864            )
865            .unwrap()
866        );
867        assert_eq!(
868            parse_datetime_partial(b"2017113010+0535").unwrap(),
869            DicomDateTime::from_date_and_time_with_time_zone(
870                DicomDate::from_ymd(2017, 11, 30).unwrap(),
871                DicomTime::from_h(10).unwrap(),
872                FixedOffset::east_opt(5 * 3600 + 35 * 60).unwrap()
873            )
874            .unwrap()
875        );
876        assert_eq!(
877            parse_datetime_partial(b"20171130-0135").unwrap(),
878            DicomDateTime::from_date_with_time_zone(
879                DicomDate::from_ymd(2017, 11, 30).unwrap(),
880                FixedOffset::west_opt(1 * 3600 + 35 * 60).unwrap()
881            )
882        );
883        assert_eq!(
884            parse_datetime_partial(b"201711-0135").unwrap(),
885            DicomDateTime::from_date_with_time_zone(
886                DicomDate::from_ym(2017, 11).unwrap(),
887                FixedOffset::west_opt(1 * 3600 + 35 * 60).unwrap()
888            )
889        );
890        assert_eq!(
891            parse_datetime_partial(b"2017-0135").unwrap(),
892            DicomDateTime::from_date_with_time_zone(
893                DicomDate::from_y(2017).unwrap(),
894                FixedOffset::west_opt(1 * 3600 + 35 * 60).unwrap()
895            )
896        );
897
898        // West UTC offset out of range
899        assert!(matches!(
900            parse_datetime_partial(b"20200101-1201"),
901            Err(Error::InvalidComponent { .. })
902        ));
903
904        // East UTC offset out of range
905        assert!(matches!(
906            parse_datetime_partial(b"20200101+1401"),
907            Err(Error::InvalidComponent { .. })
908        ));
909
910        assert!(matches!(
911            parse_datetime_partial(b"xxxx0229101010.204"),
912            Err(Error::InvalidNumberToken { .. })
913        ));
914
915        assert!(parse_datetime_partial(b"").is_err());
916        assert!(parse_datetime_partial(&[0x00_u8; 8]).is_err());
917        assert!(parse_datetime_partial(&[0xFF_u8; 8]).is_err());
918        assert!(parse_datetime_partial(&[b'0'; 8]).is_err());
919        assert!(parse_datetime_partial(&[b' '; 8]).is_err());
920        assert!(parse_datetime_partial(b"nope").is_err());
921        assert!(parse_datetime_partial(b"2015dec").is_err());
922        assert!(parse_datetime_partial(b"20151231162945.").is_err());
923        assert!(parse_datetime_partial(b"20151130161445+").is_err());
924        assert!(parse_datetime_partial(b"20151130161445+----").is_err());
925        assert!(parse_datetime_partial(b"20151130161445. ").is_err());
926        assert!(parse_datetime_partial(b"20151130161445. +0000").is_err());
927        assert!(parse_datetime_partial(b"20100423164000.001+3").is_err());
928        assert!(parse_datetime_partial(b"200809112945*1000").is_err());
929        assert!(parse_datetime_partial(b"20171130101010.204+1").is_err());
930        assert!(parse_datetime_partial(b"20171130101010.204+01").is_err());
931        assert!(parse_datetime_partial(b"20171130101010.204+011").is_err());
932    }
933}