Skip to main content

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::{FixedOffset, NaiveDate, NaiveTime};
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/// Decode the text from the byte slice into a [`DicomDateTime`] value,
333/// which allows for missing Date / Time components.
334///
335/// This is the underlying implementation of [`FromStr`](std::str::FromStr)
336/// for `DicomDateTime`.
337///
338/// # Example
339///
340/// ```
341/// # use dicom_core::value::deserialize::parse_datetime_partial;
342/// use dicom_core::value::{DicomDate, DicomDateTime, DicomTime, PreciseDateTime};
343/// use chrono::Datelike;
344///
345/// let input = "20240201123456.000305";
346/// let dt = parse_datetime_partial(input.as_bytes())?;
347/// assert_eq!(
348///     dt,
349///     DicomDateTime::from_date_and_time(
350///         DicomDate::from_ymd(2024, 2, 1).unwrap(),
351///         DicomTime::from_hms_micro(12, 34, 56, 305).unwrap(),
352///     )?
353/// );
354/// // reinterpret as a chrono date time (with or without time zone)
355/// let dt: PreciseDateTime = dt.to_precise_datetime()?;
356/// // get just the date, for example
357/// let date = dt.to_naive_date();
358/// assert_eq!(date.year(), 2024);
359/// # Ok::<_, Box<dyn std::error::Error>>(())
360/// ```
361pub fn parse_datetime_partial(buf: &[u8]) -> Result<DicomDateTime> {
362    let (date, rest) = parse_date_partial(buf)?;
363
364    let (time, buf) = match parse_time_partial(rest) {
365        Ok((time, buf)) => (Some(time), buf),
366        Err(_) => (None, rest),
367    };
368
369    let time_zone = match buf.len() {
370        0 => None,
371        len if len > 4 => {
372            let tz_sign = buf[0];
373            let buf = &buf[1..];
374            let tz_h: u32 = read_number(&buf[0..2])?;
375            let tz_m: u32 = read_number(&buf[2..4])?;
376            let s = (tz_h * 60 + tz_m) * 60;
377            match tz_sign {
378                b'+' => {
379                    check_component(DateComponent::UtcEast, &s).context(InvalidComponentSnafu)?;
380                    Some(
381                        FixedOffset::east_opt(s as i32)
382                            .context(SecsOutOfBoundsSnafu { secs: s as i32 })?,
383                    )
384                }
385                b'-' => {
386                    check_component(DateComponent::UtcWest, &s).context(InvalidComponentSnafu)?;
387                    Some(
388                        FixedOffset::west_opt(s as i32)
389                            .context(SecsOutOfBoundsSnafu { secs: s as i32 })?,
390                    )
391                }
392                c => return InvalidTimeZoneSignTokenSnafu { value: c }.fail(),
393            }
394        }
395        _ => return UnexpectedEndOfElementSnafu.fail(),
396    };
397
398    match time_zone {
399        Some(time_zone) => match time {
400            Some(tm) => DicomDateTime::from_date_and_time_with_time_zone(date, tm, time_zone)
401                .context(InvalidDateTimeSnafu),
402            None => Ok(DicomDateTime::from_date_with_time_zone(date, time_zone)),
403        },
404        None => match time {
405            Some(tm) => DicomDateTime::from_date_and_time(date, tm).context(InvalidDateTimeSnafu),
406            None => Ok(DicomDateTime::from_date(date)),
407        },
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn test_parse_date() {
417        assert_eq!(
418            parse_date(b"20180101").unwrap(),
419            NaiveDate::from_ymd_opt(2018, 1, 1).unwrap()
420        );
421        assert_eq!(
422            parse_date(b"19711231").unwrap(),
423            NaiveDate::from_ymd_opt(1971, 12, 31).unwrap()
424        );
425        assert_eq!(
426            parse_date(b"20140426").unwrap(),
427            NaiveDate::from_ymd_opt(2014, 4, 26).unwrap()
428        );
429        assert_eq!(
430            parse_date(b"20180101xxxx").unwrap(),
431            NaiveDate::from_ymd_opt(2018, 1, 1).unwrap()
432        );
433        assert_eq!(
434            parse_date(b"19000101").unwrap(),
435            NaiveDate::from_ymd_opt(1900, 1, 1).unwrap()
436        );
437        assert_eq!(
438            parse_date(b"19620728").unwrap(),
439            NaiveDate::from_ymd_opt(1962, 7, 28).unwrap()
440        );
441        assert_eq!(
442            parse_date(b"19020404-0101").unwrap(),
443            NaiveDate::from_ymd_opt(1902, 4, 4).unwrap()
444        );
445
446        assert!(matches!(
447            parse_date(b"1902"),
448            Err(Error::IncompleteValue {
449                component: DateComponent::Month,
450                ..
451            })
452        ));
453
454        assert!(matches!(
455            parse_date(b"190208"),
456            Err(Error::IncompleteValue {
457                component: DateComponent::Day,
458                ..
459            })
460        ));
461
462        assert!(matches!(
463            parse_date(b"19021515"),
464            Err(Error::InvalidComponent {
465                source: PartialValuesError::InvalidComponent {
466                    component: DateComponent::Month,
467                    value: 15,
468                    ..
469                },
470                ..
471            })
472        ));
473
474        assert!(matches!(
475            parse_date(b"19021200"),
476            Err(Error::InvalidComponent {
477                source: PartialValuesError::InvalidComponent {
478                    component: DateComponent::Day,
479                    value: 0,
480                    ..
481                },
482                ..
483            })
484        ));
485
486        assert!(matches!(
487            parse_date(b"19021232"),
488            Err(Error::InvalidComponent {
489                source: PartialValuesError::InvalidComponent {
490                    component: DateComponent::Day,
491                    value: 32,
492                    ..
493                },
494                ..
495            })
496        ));
497
498        // not a leap year
499        assert!(matches!(
500            parse_date(b"20210229"),
501            Err(Error::InvalidDate { .. })
502        ));
503
504        assert!(parse_date(b"").is_err());
505        assert!(parse_date(b"        ").is_err());
506        assert!(parse_date(b"--------").is_err());
507        assert!(parse_date(&[0x00_u8; 8]).is_err());
508        assert!(parse_date(&[0xFF_u8; 8]).is_err());
509        assert!(parse_date(&[b'0'; 8]).is_err());
510        assert!(parse_date(b"nothing!").is_err());
511        assert!(parse_date(b"2012dec").is_err());
512    }
513
514    #[test]
515    fn test_parse_date_partial() {
516        assert_eq!(
517            parse_date_partial(b"20180101").unwrap(),
518            (DicomDate::from_ymd(2018, 1, 1).unwrap(), &[][..])
519        );
520        assert_eq!(
521            parse_date_partial(b"19711231").unwrap(),
522            (DicomDate::from_ymd(1971, 12, 31).unwrap(), &[][..])
523        );
524        assert_eq!(
525            parse_date_partial(b"20180101xxxx").unwrap(),
526            (DicomDate::from_ymd(2018, 1, 1).unwrap(), &b"xxxx"[..])
527        );
528        assert_eq!(
529            parse_date_partial(b"201801xxxx").unwrap(),
530            (DicomDate::from_ym(2018, 1).unwrap(), &b"xxxx"[..])
531        );
532        assert_eq!(
533            parse_date_partial(b"2018xxxx").unwrap(),
534            (DicomDate::from_y(2018).unwrap(), &b"xxxx"[..])
535        );
536        assert_eq!(
537            parse_date_partial(b"19020404-0101").unwrap(),
538            (DicomDate::from_ymd(1902, 4, 4).unwrap(), &b"-0101"[..][..])
539        );
540        assert_eq!(
541            parse_date_partial(b"201811").unwrap(),
542            (DicomDate::from_ym(2018, 11).unwrap(), &[][..])
543        );
544        assert_eq!(
545            parse_date_partial(b"1914").unwrap(),
546            (DicomDate::from_y(1914).unwrap(), &[][..])
547        );
548
549        assert_eq!(
550            parse_date_partial(b"19140").unwrap(),
551            (DicomDate::from_y(1914).unwrap(), &b"0"[..])
552        );
553
554        assert_eq!(
555            parse_date_partial(b"1914121").unwrap(),
556            (DicomDate::from_ym(1914, 12).unwrap(), &b"1"[..])
557        );
558
559        // does not check for leap year
560        assert_eq!(
561            parse_date_partial(b"20210229").unwrap(),
562            (DicomDate::from_ymd(2021, 2, 29).unwrap(), &[][..])
563        );
564
565        assert!(matches!(
566            parse_date_partial(b"19021515"),
567            Err(Error::PartialValue {
568                source: PartialValuesError::InvalidComponent {
569                    component: DateComponent::Month,
570                    value: 15,
571                    ..
572                },
573                ..
574            })
575        ));
576
577        assert!(matches!(
578            parse_date_partial(b"19021200"),
579            Err(Error::PartialValue {
580                source: PartialValuesError::InvalidComponent {
581                    component: DateComponent::Day,
582                    value: 0,
583                    ..
584                },
585                ..
586            })
587        ));
588
589        assert!(matches!(
590            parse_date_partial(b"19021232"),
591            Err(Error::PartialValue {
592                source: PartialValuesError::InvalidComponent {
593                    component: DateComponent::Day,
594                    value: 32,
595                    ..
596                },
597                ..
598            })
599        ));
600    }
601
602    #[test]
603    fn test_parse_time() {
604        assert_eq!(
605            parse_time(b"100000.1").unwrap(),
606            (
607                NaiveTime::from_hms_micro_opt(10, 0, 0, 100_000).unwrap(),
608                &[][..]
609            )
610        );
611        assert_eq!(
612            parse_time(b"235959.0123").unwrap(),
613            (
614                NaiveTime::from_hms_micro_opt(23, 59, 59, 12_300).unwrap(),
615                &[][..]
616            )
617        );
618        // only parses 6 digit precision as in DICOM standard
619        assert_eq!(
620            parse_time(b"235959.1234567").unwrap(),
621            (
622                NaiveTime::from_hms_micro_opt(23, 59, 59, 123_456).unwrap(),
623                &b"7"[..]
624            )
625        );
626        assert_eq!(
627            parse_time(b"235959.123456+0100").unwrap(),
628            (
629                NaiveTime::from_hms_micro_opt(23, 59, 59, 123_456).unwrap(),
630                &b"+0100"[..]
631            )
632        );
633        assert_eq!(
634            parse_time(b"235959.1-0100").unwrap(),
635            (
636                NaiveTime::from_hms_micro_opt(23, 59, 59, 100_000).unwrap(),
637                &b"-0100"[..]
638            )
639        );
640        assert_eq!(
641            parse_time(b"235959.12345+0100").unwrap(),
642            (
643                NaiveTime::from_hms_micro_opt(23, 59, 59, 123_450).unwrap(),
644                &b"+0100"[..]
645            )
646        );
647        assert_eq!(
648            parse_time(b"153011").unwrap(),
649            (NaiveTime::from_hms_opt(15, 30, 11).unwrap(), &b""[..])
650        );
651        assert_eq!(
652            parse_time(b"000000.000000").unwrap(),
653            (NaiveTime::from_hms_opt(0, 0, 0).unwrap(), &[][..])
654        );
655        assert!(matches!(
656            parse_time(b"23"),
657            Err(Error::IncompleteValue {
658                component: DateComponent::Minute,
659                ..
660            })
661        ));
662        assert!(matches!(
663            parse_time(b"1530"),
664            Err(Error::IncompleteValue {
665                component: DateComponent::Second,
666                ..
667            })
668        ));
669        assert!(matches!(
670            parse_time(b"153011x0110"),
671            Err(Error::FractionDelimiter { value: 0x78_u8, .. })
672        ));
673        assert!(parse_date(&[0x00_u8; 6]).is_err());
674        assert!(parse_date(&[0xFF_u8; 6]).is_err());
675        assert!(parse_date(b"075501.----").is_err());
676        assert!(parse_date(b"nope").is_err());
677        assert!(parse_date(b"235800.0a").is_err());
678    }
679    #[test]
680    fn test_parse_time_partial() {
681        assert_eq!(
682            parse_time_partial(b"10").unwrap(),
683            (DicomTime::from_h(10).unwrap(), &[][..])
684        );
685        assert_eq!(
686            parse_time_partial(b"101").unwrap(),
687            (DicomTime::from_h(10).unwrap(), &b"1"[..])
688        );
689        assert_eq!(
690            parse_time_partial(b"0755").unwrap(),
691            (DicomTime::from_hm(7, 55).unwrap(), &[][..])
692        );
693        assert_eq!(
694            parse_time_partial(b"075500").unwrap(),
695            (DicomTime::from_hms(7, 55, 0).unwrap(), &[][..])
696        );
697        assert_eq!(
698            parse_time_partial(b"065003").unwrap(),
699            (DicomTime::from_hms(6, 50, 3).unwrap(), &[][..])
700        );
701        assert_eq!(
702            parse_time_partial(b"075501.5").unwrap(),
703            (DicomTime::from_hmsf(7, 55, 1, 5, 1).unwrap(), &[][..])
704        );
705        assert_eq!(
706            parse_time_partial(b"075501.123").unwrap(),
707            (DicomTime::from_hmsf(7, 55, 1, 123, 3).unwrap(), &[][..])
708        );
709        assert_eq!(
710            parse_time_partial(b"10+0101").unwrap(),
711            (DicomTime::from_h(10).unwrap(), &b"+0101"[..])
712        );
713        assert_eq!(
714            parse_time_partial(b"1030+0101").unwrap(),
715            (DicomTime::from_hm(10, 30).unwrap(), &b"+0101"[..])
716        );
717        assert_eq!(
718            parse_time_partial(b"075501.123+0101").unwrap(),
719            (
720                DicomTime::from_hmsf(7, 55, 1, 123, 3).unwrap(),
721                &b"+0101"[..]
722            )
723        );
724        assert_eq!(
725            parse_time_partial(b"075501+0101").unwrap(),
726            (DicomTime::from_hms(7, 55, 1).unwrap(), &b"+0101"[..])
727        );
728        assert_eq!(
729            parse_time_partial(b"075501.999999").unwrap(),
730            (DicomTime::from_hmsf(7, 55, 1, 999_999, 6).unwrap(), &[][..])
731        );
732        assert_eq!(
733            parse_time_partial(b"075501.9999994").unwrap(),
734            (
735                DicomTime::from_hmsf(7, 55, 1, 999_999, 6).unwrap(),
736                &b"4"[..]
737            )
738        );
739        // 60 seconds for leap second
740        assert_eq!(
741            parse_time_partial(b"105960").unwrap(),
742            (DicomTime::from_hms(10, 59, 60).unwrap(), &[][..])
743        );
744        assert!(matches!(
745            parse_time_partial(b"24"),
746            Err(Error::PartialValue {
747                source: PartialValuesError::InvalidComponent {
748                    component: DateComponent::Hour,
749                    value: 24,
750                    ..
751                },
752                ..
753            })
754        ));
755        assert!(matches!(
756            parse_time_partial(b"1060"),
757            Err(Error::PartialValue {
758                source: PartialValuesError::InvalidComponent {
759                    component: DateComponent::Minute,
760                    value: 60,
761                    ..
762                },
763                ..
764            })
765        ));
766    }
767
768    #[test]
769    fn test_parse_datetime_partial() {
770        assert_eq!(
771            parse_datetime_partial(b"20171130101010.204").unwrap(),
772            DicomDateTime::from_date_and_time(
773                DicomDate::from_ymd(2017, 11, 30).unwrap(),
774                DicomTime::from_hmsf(10, 10, 10, 204, 3).unwrap(),
775            )
776            .unwrap()
777        );
778        assert_eq!(
779            parse_datetime_partial(b"20171130101010").unwrap(),
780            DicomDateTime::from_date_and_time(
781                DicomDate::from_ymd(2017, 11, 30).unwrap(),
782                DicomTime::from_hms(10, 10, 10).unwrap()
783            )
784            .unwrap()
785        );
786        assert_eq!(
787            parse_datetime_partial(b"2017113023").unwrap(),
788            DicomDateTime::from_date_and_time(
789                DicomDate::from_ymd(2017, 11, 30).unwrap(),
790                DicomTime::from_h(23).unwrap()
791            )
792            .unwrap()
793        );
794        assert_eq!(
795            parse_datetime_partial(b"201711").unwrap(),
796            DicomDateTime::from_date(DicomDate::from_ym(2017, 11).unwrap())
797        );
798        assert_eq!(
799            parse_datetime_partial(b"20171130101010.204+0535").unwrap(),
800            DicomDateTime::from_date_and_time_with_time_zone(
801                DicomDate::from_ymd(2017, 11, 30).unwrap(),
802                DicomTime::from_hmsf(10, 10, 10, 204, 3).unwrap(),
803                FixedOffset::east_opt(5 * 3600 + 35 * 60).unwrap()
804            )
805            .unwrap()
806        );
807        assert_eq!(
808            parse_datetime_partial(b"20171130101010+0535").unwrap(),
809            DicomDateTime::from_date_and_time_with_time_zone(
810                DicomDate::from_ymd(2017, 11, 30).unwrap(),
811                DicomTime::from_hms(10, 10, 10).unwrap(),
812                FixedOffset::east_opt(5 * 3600 + 35 * 60).unwrap()
813            )
814            .unwrap()
815        );
816        assert_eq!(
817            parse_datetime_partial(b"2017113010+0535").unwrap(),
818            DicomDateTime::from_date_and_time_with_time_zone(
819                DicomDate::from_ymd(2017, 11, 30).unwrap(),
820                DicomTime::from_h(10).unwrap(),
821                FixedOffset::east_opt(5 * 3600 + 35 * 60).unwrap()
822            )
823            .unwrap()
824        );
825        assert_eq!(
826            parse_datetime_partial(b"20171130-0135").unwrap(),
827            DicomDateTime::from_date_with_time_zone(
828                DicomDate::from_ymd(2017, 11, 30).unwrap(),
829                FixedOffset::west_opt(3600 + 35 * 60).unwrap()
830            )
831        );
832        assert_eq!(
833            parse_datetime_partial(b"201711-0135").unwrap(),
834            DicomDateTime::from_date_with_time_zone(
835                DicomDate::from_ym(2017, 11).unwrap(),
836                FixedOffset::west_opt(3600 + 35 * 60).unwrap()
837            )
838        );
839        assert_eq!(
840            parse_datetime_partial(b"2017-0135").unwrap(),
841            DicomDateTime::from_date_with_time_zone(
842                DicomDate::from_y(2017).unwrap(),
843                FixedOffset::west_opt(3600 + 35 * 60).unwrap()
844            )
845        );
846
847        // West UTC offset out of range
848        assert!(matches!(
849            parse_datetime_partial(b"20200101-1201"),
850            Err(Error::InvalidComponent { .. })
851        ));
852
853        // East UTC offset out of range
854        assert!(matches!(
855            parse_datetime_partial(b"20200101+1401"),
856            Err(Error::InvalidComponent { .. })
857        ));
858
859        assert!(matches!(
860            parse_datetime_partial(b"xxxx0229101010.204"),
861            Err(Error::InvalidNumberToken { .. })
862        ));
863
864        assert!(parse_datetime_partial(b"").is_err());
865        assert!(parse_datetime_partial(&[0x00_u8; 8]).is_err());
866        assert!(parse_datetime_partial(&[0xFF_u8; 8]).is_err());
867        assert!(parse_datetime_partial(&[b'0'; 8]).is_err());
868        assert!(parse_datetime_partial(&[b' '; 8]).is_err());
869        assert!(parse_datetime_partial(b"nope").is_err());
870        assert!(parse_datetime_partial(b"2015dec").is_err());
871        assert!(parse_datetime_partial(b"20151231162945.").is_err());
872        assert!(parse_datetime_partial(b"20151130161445+").is_err());
873        assert!(parse_datetime_partial(b"20151130161445+----").is_err());
874        assert!(parse_datetime_partial(b"20151130161445. ").is_err());
875        assert!(parse_datetime_partial(b"20151130161445. +0000").is_err());
876        assert!(parse_datetime_partial(b"20100423164000.001+3").is_err());
877        assert!(parse_datetime_partial(b"200809112945*1000").is_err());
878        assert!(parse_datetime_partial(b"20171130101010.204+1").is_err());
879        assert!(parse_datetime_partial(b"20171130101010.204+01").is_err());
880        assert!(parse_datetime_partial(b"20171130101010.204+011").is_err());
881    }
882}