dicom_core/value/
partial.rs

1//! Handling of partial precision of Date, Time and DateTime values.
2
3use crate::value::AsRange;
4use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
5use snafu::{Backtrace, ResultExt, Snafu};
6use std::convert::{TryFrom, TryInto};
7use std::fmt;
8use std::ops::RangeInclusive;
9
10#[derive(Debug, Snafu)]
11#[non_exhaustive]
12pub enum Error {
13    #[snafu(display("To combine a DicomDate with a DicomTime value, the DicomDate has to be precise. Precision is: '{:?}'", value))]
14    DateTimeFromPartials {
15        value: DateComponent,
16        backtrace: Backtrace,
17    },
18    #[snafu(display(
19        "'{:?}' has invalid value: '{}', must be in {:?}",
20        component,
21        value,
22        range
23    ))]
24    InvalidComponent {
25        component: DateComponent,
26        value: u32,
27        range: RangeInclusive<u32>,
28        backtrace: Backtrace,
29    },
30    #[snafu(display(
31        "Second fraction precision '{}' is out of range, must be in 0..=6",
32        value
33    ))]
34    FractionPrecisionRange { value: u32, backtrace: Backtrace },
35    #[snafu(display(
36        "Number of digits in decimal representation of fraction '{}' does not match it's precision '{}'",
37        fraction,
38        precision
39    ))]
40    FractionPrecisionMismatch {
41        fraction: u32,
42        precision: u32,
43        backtrace: Backtrace,
44    },
45    #[snafu(display("Conversion of value '{}' into {:?} failed", value, component))]
46    Conversion {
47        value: String,
48        component: DateComponent,
49        source: std::num::TryFromIntError,
50    },
51    #[snafu(display(
52        "Cannot convert from an imprecise value. This value represents a date / time range"
53    ))]
54    ImpreciseValue { backtrace: Backtrace },
55}
56
57type Result<T, E = Error> = std::result::Result<T, E>;
58
59/// Represents components of Date, Time and DateTime values.
60#[derive(Debug, PartialEq, Copy, Clone, Eq, Hash, PartialOrd, Ord)]
61pub enum DateComponent {
62    // year precision
63    Year,
64    // month precision
65    Month,
66    // day precision
67    Day,
68    // hour precision
69    Hour,
70    // minute precision
71    Minute,
72    // second precision
73    Second,
74    // millisecond precision
75    Millisecond,
76    // microsecond (full second fraction)
77    Fraction,
78    // West UTC time-zone offset
79    UtcWest,
80    // East UTC time-zone offset
81    UtcEast,
82}
83
84/// Represents a Dicom date (DA) value with a partial precision,
85/// where some date components may be missing.
86///
87/// Unlike [chrono::NaiveDate], it does not allow for negative years.
88///
89/// `DicomDate` implements [AsRange] trait, enabling to retrieve specific
90/// [date](NaiveDate) values.
91///
92/// # Example
93/// ```
94/// # use std::error::Error;
95/// # use std::convert::TryFrom;
96/// use chrono::NaiveDate;
97/// use dicom_core::value::{DicomDate, AsRange};
98/// # fn main() -> Result<(), Box<dyn Error>> {
99///
100/// let date = DicomDate::from_y(1492)?;
101///
102/// assert_eq!(
103///     Some(date.latest()?),
104///     NaiveDate::from_ymd_opt(1492,12,31)
105/// );
106///
107/// let date = DicomDate::try_from(&NaiveDate::from_ymd_opt(1900, 5, 3).unwrap())?;
108/// // conversion from chrono value leads to a precise value
109/// assert_eq!(date.is_precise(), true);
110///
111/// assert_eq!(date.to_string(), "1900-05-03");
112/// # Ok(())
113/// # }
114/// ```
115#[derive(Clone, Copy, PartialEq)]
116pub struct DicomDate(DicomDateImpl);
117
118/// Represents a Dicom time (TM) value with a partial precision,
119/// where some time components may be missing.
120///
121/// Unlike [chrono::NaiveTime], this implemenation has only 6 digit precision
122/// for fraction of a second.
123///
124/// `DicomTime` implements [AsRange] trait, enabling to retrieve specific
125/// [time](NaiveTime) values.
126///
127/// # Example
128/// ```
129/// # use std::error::Error;
130/// # use std::convert::TryFrom;
131/// use chrono::NaiveTime;
132/// use dicom_core::value::{DicomTime, AsRange};
133/// # fn main() -> Result<(), Box<dyn Error>> {
134///
135/// let time = DicomTime::from_hm(12, 30)?;
136///
137/// assert_eq!(
138///     Some(time.latest()?),
139///     NaiveTime::from_hms_micro_opt(12, 30, 59, 999_999)
140/// );
141///
142/// let milli = DicomTime::from_hms_milli(12, 30, 59, 123)?;
143///
144/// // value still not precise to microsecond
145/// assert_eq!(milli.is_precise(), false);
146///
147/// assert_eq!(milli.to_string(), "12:30:59.123");
148///
149/// // for convenience, is precise enough to be retrieved as a NaiveTime
150/// assert_eq!(
151///     Some(milli.to_naive_time()?),
152///     NaiveTime::from_hms_micro_opt(12, 30, 59, 123_000)
153/// );
154///
155/// let time = DicomTime::try_from(&NaiveTime::from_hms_opt(12, 30, 59).unwrap())?;
156/// // conversion from chrono value leads to a precise value
157/// assert_eq!(time.is_precise(), true);
158///
159/// # Ok(())
160/// # }
161/// ```
162#[derive(Clone, Copy, PartialEq)]
163pub struct DicomTime(DicomTimeImpl);
164
165/// `DicomDate` is internally represented as this enum.
166/// It has 3 possible variants for YYYY, YYYYMM, YYYYMMDD values.
167#[derive(Debug, Clone, Copy, PartialEq)]
168enum DicomDateImpl {
169    Year(u16),
170    Month(u16, u8),
171    Day(u16, u8, u8),
172}
173
174/// `DicomTime` is internally represented as this enum.
175/// It has 4 possible variants.
176/// The `Fraction` variant stores the fraction second value as `u32`
177/// followed by fraction precision as `u8` ranging from 1 to 6.
178#[derive(Debug, Clone, Copy, PartialEq)]
179enum DicomTimeImpl {
180    Hour(u8),
181    Minute(u8, u8),
182    Second(u8, u8, u8),
183    Fraction(u8, u8, u8, u32, u8),
184}
185
186/// Represents a Dicom date-time (DT) value with a partial precision,
187/// where some date or time components may be missing.
188///
189/// `DicomDateTime` is always internally represented by a [DicomDate].
190/// The [DicomTime] and a timezone [FixedOffset] values are optional.
191///
192/// It implements [AsRange] trait,
193/// which serves to retrieve a [`PreciseDateTime`]
194/// from values with missing components.
195/// # Example
196/// ```
197/// # use std::error::Error;
198/// # use std::convert::TryFrom;
199/// use chrono::{DateTime, FixedOffset, TimeZone, NaiveDateTime, NaiveDate, NaiveTime};
200/// use dicom_core::value::{DicomDate, DicomTime, DicomDateTime, AsRange, PreciseDateTime};
201/// # fn main() -> Result<(), Box<dyn Error>> {
202///
203/// let offset = FixedOffset::east_opt(3600).unwrap();
204///
205/// // lets create the least precise date-time value possible 'YYYY' and make it time-zone aware
206/// let dt = DicomDateTime::from_date_with_time_zone(
207///     DicomDate::from_y(2020)?,
208///     offset
209/// );
210/// // the earliest possible value is output as a [PreciseDateTime]
211/// assert_eq!(
212///     dt.earliest()?,
213///     PreciseDateTime::TimeZone(
214///     offset.from_local_datetime(&NaiveDateTime::new(
215///         NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
216///         NaiveTime::from_hms_opt(0, 0, 0).unwrap()
217///     )).single().unwrap())
218/// );
219/// assert_eq!(
220///     dt.latest()?,
221///     PreciseDateTime::TimeZone(
222///     offset.from_local_datetime(&NaiveDateTime::new(
223///         NaiveDate::from_ymd_opt(2020, 12, 31).unwrap(),
224///         NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
225///     )).single().unwrap())
226/// );
227///
228/// let chrono_datetime = offset.from_local_datetime(&NaiveDateTime::new(
229///         NaiveDate::from_ymd_opt(2020, 12, 31).unwrap(),
230///         NaiveTime::from_hms_opt(23, 59, 0).unwrap()
231///     )).unwrap();
232///
233/// let dt = DicomDateTime::try_from(&chrono_datetime)?;
234/// // conversion from chrono value leads to a precise value
235/// assert_eq!(dt.is_precise(), true);
236///
237/// assert_eq!(dt.to_string(), "2020-12-31 23:59:00.0 +01:00");
238/// # Ok(())
239/// # }
240/// ```
241#[derive(PartialEq, Clone, Copy)]
242pub struct DicomDateTime {
243    date: DicomDate,
244    time: Option<DicomTime>,
245    time_zone: Option<FixedOffset>,
246}
247
248/**
249 * Throws a detailed `InvalidComponent` error if date / time components are out of range.
250 */
251pub fn check_component<T>(component: DateComponent, value: &T) -> Result<()>
252where
253    T: Into<u32> + Copy,
254{
255    let range = match component {
256        DateComponent::Year => 0..=9_999,
257        DateComponent::Month => 1..=12,
258        DateComponent::Day => 1..=31,
259        DateComponent::Hour => 0..=23,
260        DateComponent::Minute => 0..=59,
261        DateComponent::Second => 0..=60,
262        DateComponent::Millisecond => 0..=999,
263        DateComponent::Fraction => 0..=999_999,
264        DateComponent::UtcWest => 0..=(12 * 3600),
265        DateComponent::UtcEast => 0..=(14 * 3600),
266    };
267
268    let value: u32 = (*value).into();
269    if range.contains(&value) {
270        Ok(())
271    } else {
272        InvalidComponentSnafu {
273            component,
274            value,
275            range,
276        }
277        .fail()
278    }
279}
280
281impl DicomDate {
282    /**
283     * Constructs a new `DicomDate` with year precision
284     * (YYYY)
285     */
286    pub fn from_y(year: u16) -> Result<DicomDate> {
287        check_component(DateComponent::Year, &year)?;
288        Ok(DicomDate(DicomDateImpl::Year(year)))
289    }
290    /**
291     * Constructs a new `DicomDate` with year and month precision
292     * (YYYYMM)
293     */
294    pub fn from_ym(year: u16, month: u8) -> Result<DicomDate> {
295        check_component(DateComponent::Year, &year)?;
296        check_component(DateComponent::Month, &month)?;
297        Ok(DicomDate(DicomDateImpl::Month(year, month)))
298    }
299    /**
300     * Constructs a new `DicomDate` with a year, month and day precision
301     * (YYYYMMDD)
302     */
303    pub fn from_ymd(year: u16, month: u8, day: u8) -> Result<DicomDate> {
304        check_component(DateComponent::Year, &year)?;
305        check_component(DateComponent::Month, &month)?;
306        check_component(DateComponent::Day, &day)?;
307        Ok(DicomDate(DicomDateImpl::Day(year, month, day)))
308    }
309
310    // Retrievies the year from a date as a reference
311    pub fn year(&self) -> &u16 {
312        match self {
313            DicomDate(DicomDateImpl::Year(y)) => y,
314            DicomDate(DicomDateImpl::Month(y, _)) => y,
315            DicomDate(DicomDateImpl::Day(y, _, _)) => y,
316        }
317    }
318    // Retrievies the month from a date as a reference
319    pub fn month(&self) -> Option<&u8> {
320        match self {
321            DicomDate(DicomDateImpl::Year(_)) => None,
322            DicomDate(DicomDateImpl::Month(_, m)) => Some(m),
323            DicomDate(DicomDateImpl::Day(_, m, _)) => Some(m),
324        }
325    }
326    // Retrievies the day from a date as a reference
327    pub fn day(&self) -> Option<&u8> {
328        match self {
329            DicomDate(DicomDateImpl::Year(_)) => None,
330            DicomDate(DicomDateImpl::Month(_, _)) => None,
331            DicomDate(DicomDateImpl::Day(_, _, d)) => Some(d),
332        }
333    }
334
335    /** Retrieves the last fully precise `DateComponent` of the value */
336    pub(crate) fn precision(&self) -> DateComponent {
337        match self {
338            DicomDate(DicomDateImpl::Year(..)) => DateComponent::Year,
339            DicomDate(DicomDateImpl::Month(..)) => DateComponent::Month,
340            DicomDate(DicomDateImpl::Day(..)) => DateComponent::Day,
341        }
342    }
343}
344
345impl TryFrom<&NaiveDate> for DicomDate {
346    type Error = Error;
347    fn try_from(date: &NaiveDate) -> Result<Self> {
348        let year: u16 = date.year().try_into().with_context(|_| ConversionSnafu {
349            value: date.year().to_string(),
350            component: DateComponent::Year,
351        })?;
352        let month: u8 = date.month().try_into().with_context(|_| ConversionSnafu {
353            value: date.month().to_string(),
354            component: DateComponent::Month,
355        })?;
356        let day: u8 = date.day().try_into().with_context(|_| ConversionSnafu {
357            value: date.day().to_string(),
358            component: DateComponent::Day,
359        })?;
360        DicomDate::from_ymd(year, month, day)
361    }
362}
363
364impl fmt::Display for DicomDate {
365    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366        match self {
367            DicomDate(DicomDateImpl::Year(y)) => write!(f, "{:04}", y),
368            DicomDate(DicomDateImpl::Month(y, m)) => write!(f, "{:04}-{:02}", y, m),
369            DicomDate(DicomDateImpl::Day(y, m, d)) => write!(f, "{:04}-{:02}-{:02}", y, m, d),
370        }
371    }
372}
373
374impl fmt::Debug for DicomDate {
375    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
376        match self {
377            DicomDate(DicomDateImpl::Year(y)) => write!(f, "{:04}-MM-DD", y),
378            DicomDate(DicomDateImpl::Month(y, m)) => write!(f, "{:04}-{:02}-DD", y, m),
379            DicomDate(DicomDateImpl::Day(y, m, d)) => write!(f, "{:04}-{:02}-{:02}", y, m, d),
380        }
381    }
382}
383
384impl DicomTime {
385    /**
386     * Constructs a new `DicomTime` with hour precision
387     * (HH).
388     */
389    pub fn from_h(hour: u8) -> Result<DicomTime> {
390        check_component(DateComponent::Hour, &hour)?;
391        Ok(DicomTime(DicomTimeImpl::Hour(hour)))
392    }
393
394    /**
395     * Constructs a new `DicomTime` with hour and minute precision
396     * (HHMM).
397     */
398    pub fn from_hm(hour: u8, minute: u8) -> Result<DicomTime> {
399        check_component(DateComponent::Hour, &hour)?;
400        check_component(DateComponent::Minute, &minute)?;
401        Ok(DicomTime(DicomTimeImpl::Minute(hour, minute)))
402    }
403
404    /**
405     * Constructs a new `DicomTime` with hour, minute and second precision
406     * (HHMMSS).
407     */
408    pub fn from_hms(hour: u8, minute: u8, second: u8) -> Result<DicomTime> {
409        check_component(DateComponent::Hour, &hour)?;
410        check_component(DateComponent::Minute, &minute)?;
411        check_component(DateComponent::Second, &second)?;
412        Ok(DicomTime(DicomTimeImpl::Second(hour, minute, second)))
413    }
414    /**
415     * Constructs a new `DicomTime` from an hour, minute, second and millisecond value,
416     * which leads to a (HHMMSS.FFF) precision. Millisecond cannot exceed `999`.
417     */
418    pub fn from_hms_milli(hour: u8, minute: u8, second: u8, millisecond: u32) -> Result<DicomTime> {
419        check_component(DateComponent::Millisecond, &millisecond)?;
420        Ok(DicomTime(DicomTimeImpl::Fraction(
421            hour,
422            minute,
423            second,
424            millisecond,
425            3,
426        )))
427    }
428
429    /// Constructs a new `DicomTime` from an hour, minute, second and microsecond value,
430    /// which leads to full (`HHMMSS.FFFFFF`) precision.
431    ///
432    /// Microsecond cannot exceed `999_999`.
433    /// Instead, leap seconds can be represented by setting `second` to 60.
434    pub fn from_hms_micro(hour: u8, minute: u8, second: u8, microsecond: u32) -> Result<DicomTime> {
435        check_component(DateComponent::Fraction, &microsecond)?;
436        Ok(DicomTime(DicomTimeImpl::Fraction(
437            hour,
438            minute,
439            second,
440            microsecond,
441            6,
442        )))
443    }
444    /** Retrievies the hour from a time as a reference */
445    pub fn hour(&self) -> &u8 {
446        match self {
447            DicomTime(DicomTimeImpl::Hour(h)) => h,
448            DicomTime(DicomTimeImpl::Minute(h, _)) => h,
449            DicomTime(DicomTimeImpl::Second(h, _, _)) => h,
450            DicomTime(DicomTimeImpl::Fraction(h, _, _, _, _)) => h,
451        }
452    }
453    /** Retrievies the minute from a time as a reference */
454    pub fn minute(&self) -> Option<&u8> {
455        match self {
456            DicomTime(DicomTimeImpl::Hour(_)) => None,
457            DicomTime(DicomTimeImpl::Minute(_, m)) => Some(m),
458            DicomTime(DicomTimeImpl::Second(_, m, _)) => Some(m),
459            DicomTime(DicomTimeImpl::Fraction(_, m, _, _, _)) => Some(m),
460        }
461    }
462    /** Retrievies the minute from a time as a reference */
463    pub fn second(&self) -> Option<&u8> {
464        match self {
465            DicomTime(DicomTimeImpl::Hour(_)) => None,
466            DicomTime(DicomTimeImpl::Minute(_, _)) => None,
467            DicomTime(DicomTimeImpl::Second(_, _, s)) => Some(s),
468            DicomTime(DicomTimeImpl::Fraction(_, _, s, _, _)) => Some(s),
469        }
470    }
471    /** Retrievies the fraction of a second as a reference, if it has full (microsecond) precision. */
472    pub fn fraction(&self) -> Option<&u32> {
473        match self {
474            DicomTime(DicomTimeImpl::Hour(_)) => None,
475            DicomTime(DicomTimeImpl::Minute(_, _)) => None,
476            DicomTime(DicomTimeImpl::Second(_, _, _)) => None,
477            DicomTime(DicomTimeImpl::Fraction(_, _, _, f, fp)) => match fp {
478                6 => Some(f),
479                _ => None,
480            },
481        }
482    }
483    /** Retrievies the fraction of a second and it's precision from a time as a reference */
484    pub(crate) fn fraction_and_precision(&self) -> Option<(&u32, &u8)> {
485        match self {
486            DicomTime(DicomTimeImpl::Hour(_)) => None,
487            DicomTime(DicomTimeImpl::Minute(_, _)) => None,
488            DicomTime(DicomTimeImpl::Second(_, _, _)) => None,
489            DicomTime(DicomTimeImpl::Fraction(_, _, _, f, fp)) => Some((f, fp)),
490        }
491    }
492    /**
493     * Constructs a new `DicomTime` from an hour, minute, second, second fraction
494     * and fraction precision value (1-6). Function used for parsing only.
495     */
496    pub(crate) fn from_hmsf(
497        hour: u8,
498        minute: u8,
499        second: u8,
500        fraction: u32,
501        frac_precision: u8,
502    ) -> Result<DicomTime> {
503        if !(1..=6).contains(&frac_precision) {
504            return FractionPrecisionRangeSnafu {
505                value: frac_precision,
506            }
507            .fail();
508        }
509        if u32::pow(10, frac_precision as u32) < fraction {
510            return FractionPrecisionMismatchSnafu {
511                fraction,
512                precision: frac_precision,
513            }
514            .fail();
515        }
516
517        check_component(DateComponent::Hour, &hour)?;
518        check_component(DateComponent::Minute, &minute)?;
519        check_component(DateComponent::Second, &second)?;
520        let f: u32 = fraction * u32::pow(10, 6 - frac_precision as u32);
521        check_component(DateComponent::Fraction, &f)?;
522        Ok(DicomTime(DicomTimeImpl::Fraction(
523            hour,
524            minute,
525            second,
526            fraction,
527            frac_precision,
528        )))
529    }
530
531    /** Retrieves the last fully precise `DateComponent` of the value */
532    pub(crate) fn precision(&self) -> DateComponent {
533        match self {
534            DicomTime(DicomTimeImpl::Hour(..)) => DateComponent::Hour,
535            DicomTime(DicomTimeImpl::Minute(..)) => DateComponent::Minute,
536            DicomTime(DicomTimeImpl::Second(..)) => DateComponent::Second,
537            DicomTime(DicomTimeImpl::Fraction(..)) => DateComponent::Fraction,
538        }
539    }
540}
541
542impl TryFrom<&NaiveTime> for DicomTime {
543    type Error = Error;
544    fn try_from(time: &NaiveTime) -> Result<Self> {
545        let hour: u8 = time.hour().try_into().with_context(|_| ConversionSnafu {
546            value: time.hour().to_string(),
547            component: DateComponent::Hour,
548        })?;
549        let minute: u8 = time.minute().try_into().with_context(|_| ConversionSnafu {
550            value: time.minute().to_string(),
551            component: DateComponent::Minute,
552        })?;
553        let second: u8 = time.second().try_into().with_context(|_| ConversionSnafu {
554            value: time.second().to_string(),
555            component: DateComponent::Second,
556        })?;
557        let microsecond = time.nanosecond() / 1000;
558        // leap second correction: convert (59, 1_000_000 + x) to (60, x)
559        let (second, microsecond) = if microsecond >= 1_000_000 && second == 59 {
560            (60, microsecond - 1_000_000)
561        } else {
562            (second, microsecond)
563        };
564
565        DicomTime::from_hms_micro(hour, minute, second, microsecond)
566    }
567}
568
569impl fmt::Display for DicomTime {
570    fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result {
571        match self {
572            DicomTime(DicomTimeImpl::Hour(h)) => write!(frm, "{:02}", h),
573            DicomTime(DicomTimeImpl::Minute(h, m)) => write!(frm, "{:02}:{:02}", h, m),
574            DicomTime(DicomTimeImpl::Second(h, m, s)) => {
575                write!(frm, "{:02}:{:02}:{:02}", h, m, s)
576            }
577            DicomTime(DicomTimeImpl::Fraction(h, m, s, f, fp)) => {
578                let sfrac = (u32::pow(10, *fp as u32) + f).to_string();
579                write!(
580                    frm,
581                    "{:02}:{:02}:{:02}.{}",
582                    h,
583                    m,
584                    s,
585                    match f {
586                        0 => "0",
587                        _ => sfrac.get(1..).unwrap(),
588                    }
589                )
590            }
591        }
592    }
593}
594
595impl fmt::Debug for DicomTime {
596    fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result {
597        match self {
598            DicomTime(DicomTimeImpl::Hour(h)) => write!(frm, "{:02}:mm:ss.FFFFFF", h),
599            DicomTime(DicomTimeImpl::Minute(h, m)) => write!(frm, "{:02}:{:02}:ss.FFFFFF", h, m),
600            DicomTime(DicomTimeImpl::Second(h, m, s)) => {
601                write!(frm, "{:02}:{:02}:{:02}.FFFFFF", h, m, s)
602            }
603            DicomTime(DicomTimeImpl::Fraction(h, m, s, f, _fp)) => {
604                write!(frm, "{:02}:{:02}:{:02}.{:F<6}", h, m, s, f)
605            }
606        }
607    }
608}
609
610impl DicomDateTime {
611    /**
612     * Constructs a new `DicomDateTime` from a `DicomDate` and a timezone `FixedOffset`.
613     */
614    pub fn from_date_with_time_zone(date: DicomDate, time_zone: FixedOffset) -> DicomDateTime {
615        DicomDateTime {
616            date,
617            time: None,
618            time_zone: Some(time_zone),
619        }
620    }
621
622    /**
623     * Constructs a new `DicomDateTime` from a `DicomDate` .
624     */
625    pub fn from_date(date: DicomDate) -> DicomDateTime {
626        DicomDateTime {
627            date,
628            time: None,
629            time_zone: None,
630        }
631    }
632
633    /**
634     * Constructs a new `DicomDateTime` from a `DicomDate` and a `DicomTime`,
635     * providing that `DicomDate` is precise.
636     */
637    pub fn from_date_and_time(date: DicomDate, time: DicomTime) -> Result<DicomDateTime> {
638        if date.is_precise() {
639            Ok(DicomDateTime {
640                date,
641                time: Some(time),
642                time_zone: None,
643            })
644        } else {
645            DateTimeFromPartialsSnafu {
646                value: date.precision(),
647            }
648            .fail()
649        }
650    }
651
652    /**
653     * Constructs a new `DicomDateTime` from a `DicomDate`, `DicomTime` and a timezone `FixedOffset`,
654     * providing that `DicomDate` is precise.
655     */
656    pub fn from_date_and_time_with_time_zone(
657        date: DicomDate,
658        time: DicomTime,
659        time_zone: FixedOffset,
660    ) -> Result<DicomDateTime> {
661        if date.is_precise() {
662            Ok(DicomDateTime {
663                date,
664                time: Some(time),
665                time_zone: Some(time_zone),
666            })
667        } else {
668            DateTimeFromPartialsSnafu {
669                value: date.precision(),
670            }
671            .fail()
672        }
673    }
674
675    /** Retrieves a reference to the internal date value */
676    pub fn date(&self) -> &DicomDate {
677        &self.date
678    }
679
680    /** Retrieves a reference to the internal time value, if present */
681    pub fn time(&self) -> Option<&DicomTime> {
682        self.time.as_ref()
683    }
684
685    /** Retrieves a reference to the internal time-zone value, if present */
686    pub fn time_zone(&self) -> Option<&FixedOffset> {
687        self.time_zone.as_ref()
688    }
689
690    /** Returns true, if the `DicomDateTime` contains a time-zone */
691    pub fn has_time_zone(&self) -> bool {
692        self.time_zone.is_some()
693    }
694
695    /** Retrieves a reference to the internal offset value */
696    #[deprecated(since = "0.7.0", note = "Use `time_zone` instead")]
697    pub fn offset(&self) {}
698}
699
700impl TryFrom<&DateTime<FixedOffset>> for DicomDateTime {
701    type Error = Error;
702    fn try_from(dt: &DateTime<FixedOffset>) -> Result<Self> {
703        let year: u16 = dt.year().try_into().with_context(|_| ConversionSnafu {
704            value: dt.year().to_string(),
705            component: DateComponent::Year,
706        })?;
707        let month: u8 = dt.month().try_into().with_context(|_| ConversionSnafu {
708            value: dt.month().to_string(),
709            component: DateComponent::Month,
710        })?;
711        let day: u8 = dt.day().try_into().with_context(|_| ConversionSnafu {
712            value: dt.day().to_string(),
713            component: DateComponent::Day,
714        })?;
715        let hour: u8 = dt.hour().try_into().with_context(|_| ConversionSnafu {
716            value: dt.hour().to_string(),
717            component: DateComponent::Hour,
718        })?;
719        let minute: u8 = dt.minute().try_into().with_context(|_| ConversionSnafu {
720            value: dt.minute().to_string(),
721            component: DateComponent::Minute,
722        })?;
723        let second: u8 = dt.second().try_into().with_context(|_| ConversionSnafu {
724            value: dt.second().to_string(),
725            component: DateComponent::Second,
726        })?;
727        let microsecond = dt.nanosecond() / 1000;
728        // leap second correction: convert (59, 1_000_000 + x) to (60, x)
729        let (second, microsecond) = if microsecond >= 1_000_000 && second == 59 {
730            (60, microsecond - 1_000_000)
731        } else {
732            (second, microsecond)
733        };
734
735        DicomDateTime::from_date_and_time_with_time_zone(
736            DicomDate::from_ymd(year, month, day)?,
737            DicomTime::from_hms_micro(hour, minute, second, microsecond)?,
738            *dt.offset(),
739        )
740    }
741}
742
743impl TryFrom<&NaiveDateTime> for DicomDateTime {
744    type Error = Error;
745    fn try_from(dt: &NaiveDateTime) -> Result<Self> {
746        let year: u16 = dt.year().try_into().with_context(|_| ConversionSnafu {
747            value: dt.year().to_string(),
748            component: DateComponent::Year,
749        })?;
750        let month: u8 = dt.month().try_into().with_context(|_| ConversionSnafu {
751            value: dt.month().to_string(),
752            component: DateComponent::Month,
753        })?;
754        let day: u8 = dt.day().try_into().with_context(|_| ConversionSnafu {
755            value: dt.day().to_string(),
756            component: DateComponent::Day,
757        })?;
758        let hour: u8 = dt.hour().try_into().with_context(|_| ConversionSnafu {
759            value: dt.hour().to_string(),
760            component: DateComponent::Hour,
761        })?;
762        let minute: u8 = dt.minute().try_into().with_context(|_| ConversionSnafu {
763            value: dt.minute().to_string(),
764            component: DateComponent::Minute,
765        })?;
766        let second: u8 = dt.second().try_into().with_context(|_| ConversionSnafu {
767            value: dt.second().to_string(),
768            component: DateComponent::Second,
769        })?;
770        let microsecond = dt.nanosecond() / 1000;
771        // leap second correction: convert (59, 1_000_000 + x) to (60, x)
772        let (second, microsecond) = if microsecond >= 1_000_000 && second == 59 {
773            (60, microsecond - 1_000_000)
774        } else {
775            (second, microsecond)
776        };
777
778        DicomDateTime::from_date_and_time(
779            DicomDate::from_ymd(year, month, day)?,
780            DicomTime::from_hms_micro(hour, minute, second, microsecond)?,
781        )
782    }
783}
784
785impl fmt::Display for DicomDateTime {
786    fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result {
787        match self.time {
788            None => match self.time_zone {
789                Some(offset) => write!(frm, "{} {}", self.date, offset),
790                None => write!(frm, "{}", self.date),
791            },
792            Some(time) => match self.time_zone {
793                Some(offset) => write!(frm, "{} {} {}", self.date, time, offset),
794                None => write!(frm, "{} {}", self.date, time),
795            },
796        }
797    }
798}
799
800impl fmt::Debug for DicomDateTime {
801    fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result {
802        match self.time {
803            None => match self.time_zone {
804                Some(offset) => write!(frm, "{:?} {}", self.date, offset),
805                None => write!(frm, "{:?}", self.date),
806            },
807            Some(time) => match self.time_zone {
808                Some(offset) => write!(frm, "{:?} {:?} {}", self.date, time, offset),
809                None => write!(frm, "{:?} {:?}", self.date, time),
810            },
811        }
812    }
813}
814
815impl std::str::FromStr for DicomDateTime {
816    type Err = crate::value::DeserializeError;
817
818    fn from_str(s: &str) -> Result<Self, Self::Err> {
819        crate::value::deserialize::parse_datetime_partial(s.as_bytes())
820    }
821}
822
823impl DicomDate {
824    /**
825     * Retrieves a dicom encoded string representation of the value.
826     */
827    pub fn to_encoded(&self) -> String {
828        match self {
829            DicomDate(DicomDateImpl::Year(y)) => format!("{:04}", y),
830            DicomDate(DicomDateImpl::Month(y, m)) => format!("{:04}{:02}", y, m),
831            DicomDate(DicomDateImpl::Day(y, m, d)) => format!("{:04}{:02}{:02}", y, m, d),
832        }
833    }
834}
835
836impl DicomTime {
837    /**
838     * Retrieves a dicom encoded string representation of the value.
839     */
840    pub fn to_encoded(&self) -> String {
841        match self {
842            DicomTime(DicomTimeImpl::Hour(h)) => format!("{:02}", h),
843            DicomTime(DicomTimeImpl::Minute(h, m)) => format!("{:02}{:02}", h, m),
844            DicomTime(DicomTimeImpl::Second(h, m, s)) => format!("{:02}{:02}{:02}", h, m, s),
845            DicomTime(DicomTimeImpl::Fraction(h, m, s, f, fp)) => {
846                let sfrac = (u32::pow(10, *fp as u32) + f).to_string();
847                format!(
848                    "{:02}{:02}{:02}.{}",
849                    h,
850                    m,
851                    s,
852                    match f {
853                        0 => "0",
854                        _ => sfrac.get(1..).unwrap(),
855                    }
856                )
857            }
858        }
859    }
860}
861
862impl DicomDateTime {
863    /**
864     * Retrieves a dicom encoded string representation of the value.
865     */
866    pub fn to_encoded(&self) -> String {
867        match self.time {
868            Some(time) => match self.time_zone {
869                Some(offset) => format!(
870                    "{}{}{}",
871                    self.date.to_encoded(),
872                    time.to_encoded(),
873                    offset.to_string().replace(':', "")
874                ),
875                None => format!("{}{}", self.date.to_encoded(), time.to_encoded()),
876            },
877            None => match self.time_zone {
878                Some(offset) => format!(
879                    "{}{}",
880                    self.date.to_encoded(),
881                    offset.to_string().replace(':', "")
882                ),
883                None => self.date.to_encoded().to_string(),
884            },
885        }
886    }
887}
888
889/// An encapsulated date-time value which is precise to the microsecond
890/// and can either be time-zone aware or time-zone naive.
891///
892/// It is usually the outcome of converting a precise
893/// [DICOM date-time value](DicomDateTime)
894/// to a [chrono] date-time value.
895#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
896pub enum PreciseDateTime {
897    /// Naive date-time, with no time zone
898    Naive(NaiveDateTime),
899    /// Date-time with a time zone defined by a fixed offset
900    TimeZone(DateTime<FixedOffset>),
901}
902
903impl PreciseDateTime {
904    /// Retrieves a reference to a [`chrono::DateTime<FixedOffset>`][chrono::DateTime]
905    /// if the result is time-zone aware.
906    pub fn as_datetime(&self) -> Option<&DateTime<FixedOffset>> {
907        match self {
908            PreciseDateTime::Naive(..) => None,
909            PreciseDateTime::TimeZone(value) => Some(value),
910        }
911    }
912
913    /// Retrieves a reference to a [`chrono::NaiveDateTime`]
914    /// only if the result is time-zone naive.
915    pub fn as_naive_datetime(&self) -> Option<&NaiveDateTime> {
916        match self {
917            PreciseDateTime::Naive(value) => Some(value),
918            PreciseDateTime::TimeZone(..) => None,
919        }
920    }
921
922    /// Moves out a [`chrono::DateTime<FixedOffset>`](chrono::DateTime)
923    /// if the result is time-zone aware.
924    pub fn into_datetime(self) -> Option<DateTime<FixedOffset>> {
925        match self {
926            PreciseDateTime::Naive(..) => None,
927            PreciseDateTime::TimeZone(value) => Some(value),
928        }
929    }
930
931    /// Moves out a [`chrono::NaiveDateTime`]
932    /// only if the result is time-zone naive.
933    pub fn into_naive_datetime(self) -> Option<NaiveDateTime> {
934        match self {
935            PreciseDateTime::Naive(value) => Some(value),
936            PreciseDateTime::TimeZone(..) => None,
937        }
938    }
939
940    /// Retrieves the time-zone naive date component
941    /// of the precise date-time value.
942    ///
943    /// # Panics
944    ///
945    /// The time-zone aware variant uses `DateTime`,
946    /// which internally stores the date and time in UTC with a `NaiveDateTime`.
947    /// This method will panic if the offset from UTC would push the local date
948    /// outside of the representable range of a `NaiveDate`.
949    pub fn to_naive_date(&self) -> NaiveDate {
950        match self {
951            PreciseDateTime::Naive(value) => value.date(),
952            PreciseDateTime::TimeZone(value) => value.date_naive(),
953        }
954    }
955
956    /// Retrieves the time component of the precise date-time value.
957    pub fn to_naive_time(&self) -> NaiveTime {
958        match self {
959            PreciseDateTime::Naive(value) => value.time(),
960            PreciseDateTime::TimeZone(value) => value.time(),
961        }
962    }
963
964    /// Returns `true` if the result is time-zone aware.
965    #[inline]
966    pub fn has_time_zone(&self) -> bool {
967        matches!(self, PreciseDateTime::TimeZone(..))
968    }
969}
970
971/// The partial ordering for `PreciseDateTime`
972/// is defined by the partial ordering of matching variants
973/// (`Naive` with `Naive`, `TimeZone` with `TimeZone`).
974///
975/// Any other comparison cannot be defined,
976/// and therefore will always return `None`.
977impl PartialOrd for PreciseDateTime {
978    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
979        match (self, other) {
980            (PreciseDateTime::Naive(a), PreciseDateTime::Naive(b)) => a.partial_cmp(b),
981            (PreciseDateTime::TimeZone(a), PreciseDateTime::TimeZone(b)) => a.partial_cmp(b),
982            _ => None,
983        }
984    }
985}
986
987#[cfg(test)]
988mod tests {
989    use super::*;
990    use chrono::TimeZone;
991
992    #[test]
993    fn test_dicom_date() {
994        assert_eq!(
995            DicomDate::from_ymd(1944, 2, 29).unwrap(),
996            DicomDate(DicomDateImpl::Day(1944, 2, 29))
997        );
998
999        // cheap precision check, but date is invalid
1000        assert!(DicomDate::from_ymd(1945, 2, 29).unwrap().is_precise());
1001        assert_eq!(
1002            DicomDate::from_ym(1944, 2).unwrap(),
1003            DicomDate(DicomDateImpl::Month(1944, 2))
1004        );
1005        assert_eq!(
1006            DicomDate::from_y(1944).unwrap(),
1007            DicomDate(DicomDateImpl::Year(1944))
1008        );
1009
1010        assert_eq!(DicomDate::from_ymd(1944, 2, 29).unwrap().is_precise(), true);
1011        assert_eq!(DicomDate::from_ym(1944, 2).unwrap().is_precise(), false);
1012        assert_eq!(DicomDate::from_y(1944).unwrap().is_precise(), false);
1013        assert_eq!(
1014            DicomDate::from_ymd(1944, 2, 29)
1015                .unwrap()
1016                .earliest()
1017                .unwrap(),
1018            NaiveDate::from_ymd_opt(1944, 2, 29).unwrap()
1019        );
1020        assert_eq!(
1021            DicomDate::from_ymd(1944, 2, 29).unwrap().latest().unwrap(),
1022            NaiveDate::from_ymd_opt(1944, 2, 29).unwrap()
1023        );
1024
1025        assert_eq!(
1026            DicomDate::from_y(1944).unwrap().earliest().unwrap(),
1027            NaiveDate::from_ymd_opt(1944, 1, 1).unwrap()
1028        );
1029        // detects leap year
1030        assert_eq!(
1031            DicomDate::from_ym(1944, 2).unwrap().latest().unwrap(),
1032            NaiveDate::from_ymd_opt(1944, 2, 29).unwrap()
1033        );
1034        assert_eq!(
1035            DicomDate::from_ym(1945, 2).unwrap().latest().unwrap(),
1036            NaiveDate::from_ymd_opt(1945, 2, 28).unwrap()
1037        );
1038
1039        assert_eq!(
1040            DicomDate::try_from(&NaiveDate::from_ymd_opt(1945, 2, 28).unwrap()).unwrap(),
1041            DicomDate(DicomDateImpl::Day(1945, 2, 28))
1042        );
1043
1044        assert!(matches!(
1045            DicomDate::try_from(&NaiveDate::from_ymd_opt(-2000, 2, 28).unwrap()),
1046            Err(Error::Conversion { .. })
1047        ));
1048
1049        assert!(matches!(
1050            DicomDate::try_from(&NaiveDate::from_ymd_opt(10_000, 2, 28).unwrap()),
1051            Err(Error::InvalidComponent {
1052                component: DateComponent::Year,
1053                ..
1054            })
1055        ));
1056    }
1057
1058    #[test]
1059    fn test_dicom_time() {
1060        assert_eq!(
1061            DicomTime::from_hms_micro(9, 1, 1, 123456).unwrap(),
1062            DicomTime(DicomTimeImpl::Fraction(9, 1, 1, 123456, 6))
1063        );
1064        assert_eq!(
1065            DicomTime::from_hms_micro(9, 1, 1, 1).unwrap(),
1066            DicomTime(DicomTimeImpl::Fraction(9, 1, 1, 1, 6))
1067        );
1068        assert_eq!(
1069            DicomTime::from_hms(9, 0, 0).unwrap(),
1070            DicomTime(DicomTimeImpl::Second(9, 0, 0))
1071        );
1072        assert_eq!(
1073            DicomTime::from_hm(23, 59).unwrap(),
1074            DicomTime(DicomTimeImpl::Minute(23, 59))
1075        );
1076        assert_eq!(
1077            DicomTime::from_h(1).unwrap(),
1078            DicomTime(DicomTimeImpl::Hour(1))
1079        );
1080        // cheap precision checks
1081        assert!(DicomTime::from_hms_micro(9, 1, 1, 123456)
1082            .unwrap()
1083            .is_precise());
1084        assert!(!DicomTime::from_hms_milli(9, 1, 1, 123)
1085            .unwrap()
1086            .is_precise());
1087
1088        assert_eq!(
1089            DicomTime::from_hms_milli(9, 1, 1, 123)
1090                .unwrap()
1091                .earliest()
1092                .unwrap(),
1093            NaiveTime::from_hms_micro_opt(9, 1, 1, 123_000).unwrap()
1094        );
1095        assert_eq!(
1096            DicomTime::from_hms_milli(9, 1, 1, 123)
1097                .unwrap()
1098                .latest()
1099                .unwrap(),
1100            NaiveTime::from_hms_micro_opt(9, 1, 1, 123_999).unwrap()
1101        );
1102
1103        assert_eq!(
1104            DicomTime::from_hms_milli(9, 1, 1, 2)
1105                .unwrap()
1106                .earliest()
1107                .unwrap(),
1108            NaiveTime::from_hms_micro_opt(9, 1, 1, 002000).unwrap()
1109        );
1110        assert_eq!(
1111            DicomTime::from_hms_milli(9, 1, 1, 2)
1112                .unwrap()
1113                .latest()
1114                .unwrap(),
1115            NaiveTime::from_hms_micro_opt(9, 1, 1, 002999).unwrap()
1116        );
1117
1118        assert_eq!(
1119            DicomTime::from_hms_micro(9, 1, 1, 123456)
1120                .unwrap()
1121                .is_precise(),
1122            true
1123        );
1124
1125        assert_eq!(
1126            DicomTime::from_hms_milli(9, 1, 1, 1).unwrap(),
1127            DicomTime(DicomTimeImpl::Fraction(9, 1, 1, 1, 3))
1128        );
1129
1130        assert_eq!(
1131            DicomTime::try_from(&NaiveTime::from_hms_milli_opt(16, 31, 28, 123).unwrap()).unwrap(),
1132            DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 123_000, 6))
1133        );
1134
1135        assert_eq!(
1136            DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 28, 123).unwrap()).unwrap(),
1137            DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 000123, 6))
1138        );
1139
1140        assert_eq!(
1141            DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 28, 1234).unwrap()).unwrap(),
1142            DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 001234, 6))
1143        );
1144
1145        assert_eq!(
1146            DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 28, 0).unwrap()).unwrap(),
1147            DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 0, 6))
1148        );
1149
1150        assert_eq!(
1151            DicomTime::from_hmsf(9, 1, 1, 1, 4).unwrap().to_string(),
1152            "09:01:01.0001"
1153        );
1154        assert_eq!(
1155            DicomTime::from_hmsf(9, 1, 1, 0, 1).unwrap().to_string(),
1156            "09:01:01.0"
1157        );
1158        assert_eq!(
1159            DicomTime::from_hmsf(7, 55, 1, 1, 5).unwrap().to_encoded(),
1160            "075501.00001"
1161        );
1162        // any precision for zero is just one zero
1163        assert_eq!(
1164            DicomTime::from_hmsf(9, 1, 1, 0, 6).unwrap().to_encoded(),
1165            "090101.0"
1166        );
1167
1168        // leap second allowed here
1169        assert_eq!(
1170            DicomTime::from_hmsf(23, 59, 60, 123, 3)
1171                .unwrap()
1172                .to_encoded(),
1173            "235960.123",
1174        );
1175
1176        // leap second from chrono NaiveTime is admitted
1177        assert_eq!(
1178            DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 59, 1_000_000).unwrap())
1179                .unwrap()
1180                .to_encoded(),
1181            "163160.0",
1182        );
1183
1184        // sub-second precision after leap second from NaiveTime is admitted
1185        assert_eq!(
1186            DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 59, 1_012_345).unwrap())
1187                .unwrap()
1188                .to_encoded(),
1189            "163160.012345",
1190        );
1191
1192        assert!(matches!(
1193            DicomTime::from_hmsf(9, 1, 1, 1, 7),
1194            Err(Error::FractionPrecisionRange { value: 7, .. })
1195        ));
1196
1197        assert!(matches!(
1198            DicomTime::from_hms_milli(9, 1, 1, 1000),
1199            Err(Error::InvalidComponent {
1200                component: DateComponent::Millisecond,
1201                ..
1202            })
1203        ));
1204
1205        assert!(matches!(
1206            DicomTime::from_hmsf(9, 1, 1, 123456, 3),
1207            Err(Error::FractionPrecisionMismatch {
1208                fraction: 123456,
1209                precision: 3,
1210                ..
1211            })
1212        ));
1213
1214        // invalid second fraction: leap second not allowed here
1215        assert!(matches!(
1216            DicomTime::from_hmsf(9, 1, 1, 1_000_000, 6),
1217            Err(Error::InvalidComponent {
1218                component: DateComponent::Fraction,
1219                ..
1220            })
1221        ));
1222
1223        assert!(matches!(
1224            DicomTime::from_hmsf(9, 1, 1, 12345, 5).unwrap().exact(),
1225            Err(crate::value::range::Error::ImpreciseValue { .. })
1226        ));
1227    }
1228
1229    #[test]
1230    fn test_dicom_datetime() {
1231        let default_offset = FixedOffset::east_opt(0).unwrap();
1232        assert_eq!(
1233            DicomDateTime::from_date_with_time_zone(
1234                DicomDate::from_ymd(2020, 2, 29).unwrap(),
1235                default_offset
1236            ),
1237            DicomDateTime {
1238                date: DicomDate::from_ymd(2020, 2, 29).unwrap(),
1239                time: None,
1240                time_zone: Some(default_offset)
1241            }
1242        );
1243
1244        assert_eq!(
1245            DicomDateTime::from_date(DicomDate::from_ym(2020, 2).unwrap())
1246                .earliest()
1247                .unwrap(),
1248            PreciseDateTime::Naive(NaiveDateTime::new(
1249                NaiveDate::from_ymd_opt(2020, 2, 1).unwrap(),
1250                NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1251            ))
1252        );
1253
1254        assert_eq!(
1255            DicomDateTime::from_date_with_time_zone(
1256                DicomDate::from_ym(2020, 2).unwrap(),
1257                default_offset
1258            )
1259            .latest()
1260            .unwrap(),
1261            PreciseDateTime::TimeZone(
1262                FixedOffset::east_opt(0)
1263                    .unwrap()
1264                    .from_local_datetime(&NaiveDateTime::new(
1265                        NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1266                        NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1267                    ))
1268                    .unwrap()
1269            )
1270        );
1271
1272        assert_eq!(
1273            DicomDateTime::from_date_and_time_with_time_zone(
1274                DicomDate::from_ymd(2020, 2, 29).unwrap(),
1275                DicomTime::from_hmsf(23, 59, 59, 10, 2).unwrap(),
1276                default_offset
1277            )
1278            .unwrap()
1279            .earliest()
1280            .unwrap(),
1281            PreciseDateTime::TimeZone(
1282                FixedOffset::east_opt(0)
1283                    .unwrap()
1284                    .from_local_datetime(&NaiveDateTime::new(
1285                        NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1286                        NaiveTime::from_hms_micro_opt(23, 59, 59, 100_000).unwrap()
1287                    ))
1288                    .unwrap()
1289            )
1290        );
1291        assert_eq!(
1292            DicomDateTime::from_date_and_time_with_time_zone(
1293                DicomDate::from_ymd(2020, 2, 29).unwrap(),
1294                DicomTime::from_hmsf(23, 59, 59, 10, 2).unwrap(),
1295                default_offset
1296            )
1297            .unwrap()
1298            .latest()
1299            .unwrap(),
1300            PreciseDateTime::TimeZone(
1301                FixedOffset::east_opt(0)
1302                    .unwrap()
1303                    .from_local_datetime(&NaiveDateTime::new(
1304                        NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1305                        NaiveTime::from_hms_micro_opt(23, 59, 59, 109_999).unwrap()
1306                    ))
1307                    .unwrap()
1308            )
1309        );
1310
1311        assert_eq!(
1312            DicomDateTime::try_from(
1313                &FixedOffset::east_opt(0)
1314                    .unwrap()
1315                    .from_local_datetime(&NaiveDateTime::new(
1316                        NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1317                        NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1318                    ))
1319                    .unwrap()
1320            )
1321            .unwrap(),
1322            DicomDateTime {
1323                date: DicomDate::from_ymd(2020, 2, 29).unwrap(),
1324                time: Some(DicomTime::from_hms_micro(23, 59, 59, 999_999).unwrap()),
1325                time_zone: Some(default_offset)
1326            }
1327        );
1328
1329        assert_eq!(
1330            DicomDateTime::try_from(
1331                &FixedOffset::east_opt(0)
1332                    .unwrap()
1333                    .from_local_datetime(&NaiveDateTime::new(
1334                        NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1335                        NaiveTime::from_hms_micro_opt(23, 59, 59, 0).unwrap()
1336                    ))
1337                    .unwrap()
1338            )
1339            .unwrap(),
1340            DicomDateTime {
1341                date: DicomDate::from_ymd(2020, 2, 29).unwrap(),
1342                time: Some(DicomTime::from_hms_micro(23, 59, 59, 0).unwrap()),
1343                time_zone: Some(default_offset)
1344            }
1345        );
1346
1347        // leap second from chrono NaiveTime is admitted
1348        assert_eq!(
1349            DicomDateTime::try_from(
1350                &FixedOffset::east_opt(0)
1351                    .unwrap()
1352                    .from_local_datetime(&NaiveDateTime::new(
1353                        NaiveDate::from_ymd_opt(2023, 12, 31).unwrap(),
1354                        NaiveTime::from_hms_micro_opt(23, 59, 59, 1_000_000).unwrap()
1355                    ))
1356                    .unwrap()
1357            )
1358            .unwrap(),
1359            DicomDateTime {
1360                date: DicomDate::from_ymd(2023, 12, 31).unwrap(),
1361                time: Some(DicomTime::from_hms_micro(23, 59, 60, 0).unwrap()),
1362                time_zone: Some(default_offset)
1363            }
1364        );
1365
1366        assert!(matches!(
1367            DicomDateTime::from_date_with_time_zone(
1368                DicomDate::from_ymd(2021, 2, 29).unwrap(),
1369                default_offset
1370            )
1371            .earliest(),
1372            Err(crate::value::range::Error::InvalidDate { .. })
1373        ));
1374
1375        assert!(matches!(
1376            DicomDateTime::from_date_and_time_with_time_zone(
1377                DicomDate::from_ym(2020, 2).unwrap(),
1378                DicomTime::from_hms_milli(23, 59, 59, 999).unwrap(),
1379                default_offset
1380            ),
1381            Err(Error::DateTimeFromPartials {
1382                value: DateComponent::Month,
1383                ..
1384            })
1385        ));
1386        assert!(matches!(
1387            DicomDateTime::from_date_and_time_with_time_zone(
1388                DicomDate::from_y(1).unwrap(),
1389                DicomTime::from_hms_micro(23, 59, 59, 10).unwrap(),
1390                default_offset
1391            ),
1392            Err(Error::DateTimeFromPartials {
1393                value: DateComponent::Year,
1394                ..
1395            })
1396        ));
1397
1398        assert!(matches!(
1399            DicomDateTime::from_date_and_time_with_time_zone(
1400                DicomDate::from_ymd(2000, 1, 1).unwrap(),
1401                DicomTime::from_hms_milli(23, 59, 59, 10).unwrap(),
1402                default_offset
1403            )
1404            .unwrap()
1405            .exact(),
1406            Err(crate::value::range::Error::ImpreciseValue { .. })
1407        ));
1408
1409        // simple precision checks
1410        assert!(
1411            DicomDateTime::from_date_and_time(
1412                DicomDate::from_ymd(2000, 1, 1).unwrap(),
1413                DicomTime::from_hms_milli(23, 59, 59, 10).unwrap()
1414            )
1415            .unwrap()
1416            .is_precise()
1417                == false
1418        );
1419
1420        assert!(DicomDateTime::from_date_and_time(
1421            DicomDate::from_ymd(2000, 1, 1).unwrap(),
1422            DicomTime::from_hms_micro(23, 59, 59, 654_321).unwrap()
1423        )
1424        .unwrap()
1425        .is_precise());
1426    }
1427}