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 implementation 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    /// Retrieves 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    /// Retrieves 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    /// Retrieves 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 the precision `HHMMSS.FFF`. 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 the full precision `HHMMSS.FFFFFF`.
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    /** Retrieves 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    /** Retrieves 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    /** Retrieves 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    /** Retrieves 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    /** Retrieves 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!("{:02}{:02}{:02}.{}", h, m, s, sfrac.get(1..).unwrap())
848            }
849        }
850    }
851}
852
853impl DicomDateTime {
854    /**
855     * Retrieves a dicom encoded string representation of the value.
856     */
857    pub fn to_encoded(&self) -> String {
858        match self.time {
859            Some(time) => match self.time_zone {
860                Some(offset) => format!(
861                    "{}{}{}",
862                    self.date.to_encoded(),
863                    time.to_encoded(),
864                    offset.to_string().replace(':', "")
865                ),
866                None => format!("{}{}", self.date.to_encoded(), time.to_encoded()),
867            },
868            None => match self.time_zone {
869                Some(offset) => format!(
870                    "{}{}",
871                    self.date.to_encoded(),
872                    offset.to_string().replace(':', "")
873                ),
874                None => self.date.to_encoded().to_string(),
875            },
876        }
877    }
878}
879
880/// An encapsulated date-time value which is precise to the microsecond
881/// and can either be time-zone aware or time-zone naive.
882///
883/// It is usually the outcome of converting a precise
884/// [DICOM date-time value](DicomDateTime)
885/// to a [chrono] date-time value.
886#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
887pub enum PreciseDateTime {
888    /// Naive date-time, with no time zone
889    Naive(NaiveDateTime),
890    /// Date-time with a time zone defined by a fixed offset
891    TimeZone(DateTime<FixedOffset>),
892}
893
894impl PreciseDateTime {
895    /// Retrieves a reference to a [`chrono::DateTime<FixedOffset>`][chrono::DateTime]
896    /// if the result is time-zone aware.
897    pub fn as_datetime(&self) -> Option<&DateTime<FixedOffset>> {
898        match self {
899            PreciseDateTime::Naive(..) => None,
900            PreciseDateTime::TimeZone(value) => Some(value),
901        }
902    }
903
904    /// Retrieves a reference to a [`chrono::NaiveDateTime`]
905    /// only if the result is time-zone naive.
906    pub fn as_naive_datetime(&self) -> Option<&NaiveDateTime> {
907        match self {
908            PreciseDateTime::Naive(value) => Some(value),
909            PreciseDateTime::TimeZone(..) => None,
910        }
911    }
912
913    /// Moves out a [`chrono::DateTime<FixedOffset>`](chrono::DateTime)
914    /// if the result is time-zone aware.
915    pub fn into_datetime(self) -> Option<DateTime<FixedOffset>> {
916        match self {
917            PreciseDateTime::Naive(..) => None,
918            PreciseDateTime::TimeZone(value) => Some(value),
919        }
920    }
921
922    /// Moves out a [`chrono::NaiveDateTime`]
923    /// only if the result is time-zone naive.
924    pub fn into_naive_datetime(self) -> Option<NaiveDateTime> {
925        match self {
926            PreciseDateTime::Naive(value) => Some(value),
927            PreciseDateTime::TimeZone(..) => None,
928        }
929    }
930
931    /// Retrieves the time-zone naive date component
932    /// of the precise date-time value.
933    ///
934    /// # Panics
935    ///
936    /// The time-zone aware variant uses `DateTime`,
937    /// which internally stores the date and time in UTC with a `NaiveDateTime`.
938    /// This method will panic if the offset from UTC would push the local date
939    /// outside of the representable range of a `NaiveDate`.
940    pub fn to_naive_date(&self) -> NaiveDate {
941        match self {
942            PreciseDateTime::Naive(value) => value.date(),
943            PreciseDateTime::TimeZone(value) => value.date_naive(),
944        }
945    }
946
947    /// Retrieves the time component of the precise date-time value.
948    pub fn to_naive_time(&self) -> NaiveTime {
949        match self {
950            PreciseDateTime::Naive(value) => value.time(),
951            PreciseDateTime::TimeZone(value) => value.time(),
952        }
953    }
954
955    /// Returns `true` if the result is time-zone aware.
956    #[inline]
957    pub fn has_time_zone(&self) -> bool {
958        matches!(self, PreciseDateTime::TimeZone(..))
959    }
960}
961
962/// The partial ordering for `PreciseDateTime`
963/// is defined by the partial ordering of matching variants
964/// (`Naive` with `Naive`, `TimeZone` with `TimeZone`).
965///
966/// Any other comparison cannot be defined,
967/// and therefore will always return `None`.
968impl PartialOrd for PreciseDateTime {
969    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
970        match (self, other) {
971            (PreciseDateTime::Naive(a), PreciseDateTime::Naive(b)) => a.partial_cmp(b),
972            (PreciseDateTime::TimeZone(a), PreciseDateTime::TimeZone(b)) => a.partial_cmp(b),
973            _ => None,
974        }
975    }
976}
977
978#[cfg(test)]
979mod tests {
980    use super::*;
981    use chrono::TimeZone;
982
983    #[test]
984    fn test_dicom_date() {
985        assert_eq!(
986            DicomDate::from_ymd(1944, 2, 29).unwrap(),
987            DicomDate(DicomDateImpl::Day(1944, 2, 29))
988        );
989
990        // cheap precision check, but date is invalid
991        assert!(DicomDate::from_ymd(1945, 2, 29).unwrap().is_precise());
992        assert_eq!(
993            DicomDate::from_ym(1944, 2).unwrap(),
994            DicomDate(DicomDateImpl::Month(1944, 2))
995        );
996        assert_eq!(
997            DicomDate::from_y(1944).unwrap(),
998            DicomDate(DicomDateImpl::Year(1944))
999        );
1000
1001        assert_eq!(DicomDate::from_ymd(1944, 2, 29).unwrap().is_precise(), true);
1002        assert_eq!(DicomDate::from_ym(1944, 2).unwrap().is_precise(), false);
1003        assert_eq!(DicomDate::from_y(1944).unwrap().is_precise(), false);
1004        assert_eq!(
1005            DicomDate::from_ymd(1944, 2, 29)
1006                .unwrap()
1007                .earliest()
1008                .unwrap(),
1009            NaiveDate::from_ymd_opt(1944, 2, 29).unwrap()
1010        );
1011        assert_eq!(
1012            DicomDate::from_ymd(1944, 2, 29).unwrap().latest().unwrap(),
1013            NaiveDate::from_ymd_opt(1944, 2, 29).unwrap()
1014        );
1015
1016        assert_eq!(
1017            DicomDate::from_y(1944).unwrap().earliest().unwrap(),
1018            NaiveDate::from_ymd_opt(1944, 1, 1).unwrap()
1019        );
1020        // detects leap year
1021        assert_eq!(
1022            DicomDate::from_ym(1944, 2).unwrap().latest().unwrap(),
1023            NaiveDate::from_ymd_opt(1944, 2, 29).unwrap()
1024        );
1025        assert_eq!(
1026            DicomDate::from_ym(1945, 2).unwrap().latest().unwrap(),
1027            NaiveDate::from_ymd_opt(1945, 2, 28).unwrap()
1028        );
1029
1030        assert_eq!(
1031            DicomDate::try_from(&NaiveDate::from_ymd_opt(1945, 2, 28).unwrap()).unwrap(),
1032            DicomDate(DicomDateImpl::Day(1945, 2, 28))
1033        );
1034
1035        assert!(matches!(
1036            DicomDate::try_from(&NaiveDate::from_ymd_opt(-2000, 2, 28).unwrap()),
1037            Err(Error::Conversion { .. })
1038        ));
1039
1040        assert!(matches!(
1041            DicomDate::try_from(&NaiveDate::from_ymd_opt(10_000, 2, 28).unwrap()),
1042            Err(Error::InvalidComponent {
1043                component: DateComponent::Year,
1044                ..
1045            })
1046        ));
1047    }
1048
1049    #[test]
1050    fn test_dicom_time() {
1051        assert_eq!(
1052            DicomTime::from_hms_micro(9, 1, 1, 123456).unwrap(),
1053            DicomTime(DicomTimeImpl::Fraction(9, 1, 1, 123456, 6))
1054        );
1055        assert_eq!(
1056            DicomTime::from_hms_micro(9, 1, 1, 1).unwrap(),
1057            DicomTime(DicomTimeImpl::Fraction(9, 1, 1, 1, 6))
1058        );
1059        assert_eq!(
1060            DicomTime::from_hms(9, 0, 0).unwrap(),
1061            DicomTime(DicomTimeImpl::Second(9, 0, 0))
1062        );
1063        assert_eq!(
1064            DicomTime::from_hm(23, 59).unwrap(),
1065            DicomTime(DicomTimeImpl::Minute(23, 59))
1066        );
1067        assert_eq!(
1068            DicomTime::from_h(1).unwrap(),
1069            DicomTime(DicomTimeImpl::Hour(1))
1070        );
1071        // cheap precision checks
1072        assert!(DicomTime::from_hms_micro(9, 1, 1, 123456)
1073            .unwrap()
1074            .is_precise());
1075        assert!(!DicomTime::from_hms_milli(9, 1, 1, 123)
1076            .unwrap()
1077            .is_precise());
1078
1079        assert_eq!(
1080            DicomTime::from_hms_milli(9, 1, 1, 123)
1081                .unwrap()
1082                .earliest()
1083                .unwrap(),
1084            NaiveTime::from_hms_micro_opt(9, 1, 1, 123_000).unwrap()
1085        );
1086        assert_eq!(
1087            DicomTime::from_hms_milli(9, 1, 1, 123)
1088                .unwrap()
1089                .latest()
1090                .unwrap(),
1091            NaiveTime::from_hms_micro_opt(9, 1, 1, 123_999).unwrap()
1092        );
1093
1094        assert_eq!(
1095            DicomTime::from_hms_milli(9, 1, 1, 2)
1096                .unwrap()
1097                .earliest()
1098                .unwrap(),
1099            NaiveTime::from_hms_micro_opt(9, 1, 1, 002000).unwrap()
1100        );
1101        assert_eq!(
1102            DicomTime::from_hms_milli(9, 1, 1, 2)
1103                .unwrap()
1104                .latest()
1105                .unwrap(),
1106            NaiveTime::from_hms_micro_opt(9, 1, 1, 002999).unwrap()
1107        );
1108
1109        assert_eq!(
1110            DicomTime::from_hms_micro(9, 1, 1, 123456)
1111                .unwrap()
1112                .is_precise(),
1113            true
1114        );
1115
1116        assert_eq!(
1117            DicomTime::from_hms_milli(9, 1, 1, 1).unwrap(),
1118            DicomTime(DicomTimeImpl::Fraction(9, 1, 1, 1, 3))
1119        );
1120
1121        assert_eq!(
1122            DicomTime::try_from(&NaiveTime::from_hms_milli_opt(16, 31, 28, 123).unwrap()).unwrap(),
1123            DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 123_000, 6))
1124        );
1125
1126        assert_eq!(
1127            DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 28, 123).unwrap()).unwrap(),
1128            DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 000123, 6))
1129        );
1130
1131        assert_eq!(
1132            DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 28, 1234).unwrap()).unwrap(),
1133            DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 001234, 6))
1134        );
1135
1136        assert_eq!(
1137            DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 28, 0).unwrap()).unwrap(),
1138            DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 0, 6))
1139        );
1140
1141        assert_eq!(
1142            DicomTime::from_hmsf(9, 1, 1, 1, 4).unwrap().to_string(),
1143            "09:01:01.0001"
1144        );
1145        assert_eq!(
1146            DicomTime::from_hmsf(9, 1, 1, 0, 1).unwrap().to_string(),
1147            "09:01:01.0"
1148        );
1149        assert_eq!(
1150            DicomTime::from_hmsf(7, 55, 1, 1, 5).unwrap().to_encoded(),
1151            "075501.00001"
1152        );
1153        // the number of trailing zeros always complies with precision
1154        assert_eq!(
1155            DicomTime::from_hmsf(9, 1, 1, 0, 2).unwrap().to_encoded(),
1156            "090101.00"
1157        );
1158        assert_eq!(
1159            DicomTime::from_hmsf(9, 1, 1, 0, 3).unwrap().to_encoded(),
1160            "090101.000"
1161        );
1162        assert_eq!(
1163            DicomTime::from_hmsf(9, 1, 1, 0, 4).unwrap().to_encoded(),
1164            "090101.0000"
1165        );
1166        assert_eq!(
1167            DicomTime::from_hmsf(9, 1, 1, 0, 5).unwrap().to_encoded(),
1168            "090101.00000"
1169        );
1170        assert_eq!(
1171            DicomTime::from_hmsf(9, 1, 1, 0, 6).unwrap().to_encoded(),
1172            "090101.000000"
1173        );
1174
1175        // leap second allowed here
1176        assert_eq!(
1177            DicomTime::from_hmsf(23, 59, 60, 123, 3)
1178                .unwrap()
1179                .to_encoded(),
1180            "235960.123",
1181        );
1182
1183        // leap second from chrono NaiveTime is admitted
1184        assert_eq!(
1185            DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 59, 1_000_000).unwrap())
1186                .unwrap()
1187                .to_encoded(),
1188            "163160.000000",
1189        );
1190
1191        // sub-second precision after leap second from NaiveTime is admitted
1192        assert_eq!(
1193            DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 59, 1_012_345).unwrap())
1194                .unwrap()
1195                .to_encoded(),
1196            "163160.012345",
1197        );
1198
1199
1200        // time specifically with 0 microseconds
1201        assert_eq!(
1202            DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 59, 0).unwrap())
1203                .unwrap()
1204                .to_encoded(),
1205            "163159.000000",
1206        );
1207
1208        // specific date-time from chrono
1209        let date_time: DateTime<_> = DateTime::<chrono::Utc>::from_naive_utc_and_offset(
1210            NaiveDateTime::new(
1211                NaiveDate::from_ymd_opt(2024, 8, 9).unwrap(),
1212                NaiveTime::from_hms_opt(9, 9, 39).unwrap(),
1213            ),
1214            chrono::Utc,
1215        ).with_timezone(&FixedOffset::east_opt(0).unwrap());
1216        let dicom_date_time = DicomDateTime::try_from(&date_time).unwrap();
1217        assert!(dicom_date_time.has_time_zone());
1218        assert!(dicom_date_time.is_precise());
1219        let dicom_time = dicom_date_time.time().unwrap();
1220        assert_eq!(
1221            dicom_time.fraction_and_precision(),
1222            Some((&0, &6)),
1223        );
1224        assert_eq!(
1225            dicom_date_time.to_encoded(),
1226            "20240809090939.000000+0000"
1227        );
1228
1229        // bad inputs
1230
1231        assert!(matches!(
1232            DicomTime::from_hmsf(9, 1, 1, 1, 7),
1233            Err(Error::FractionPrecisionRange { value: 7, .. })
1234        ));
1235
1236        assert!(matches!(
1237            DicomTime::from_hms_milli(9, 1, 1, 1000),
1238            Err(Error::InvalidComponent {
1239                component: DateComponent::Millisecond,
1240                ..
1241            })
1242        ));
1243
1244        assert!(matches!(
1245            DicomTime::from_hmsf(9, 1, 1, 123456, 3),
1246            Err(Error::FractionPrecisionMismatch {
1247                fraction: 123456,
1248                precision: 3,
1249                ..
1250            })
1251        ));
1252
1253        // invalid second fraction: leap second not allowed here
1254        assert!(matches!(
1255            DicomTime::from_hmsf(9, 1, 1, 1_000_000, 6),
1256            Err(Error::InvalidComponent {
1257                component: DateComponent::Fraction,
1258                ..
1259            })
1260        ));
1261
1262        assert!(matches!(
1263            DicomTime::from_hmsf(9, 1, 1, 12345, 5).unwrap().exact(),
1264            Err(crate::value::range::Error::ImpreciseValue { .. })
1265        ));
1266    }
1267
1268    #[test]
1269    fn test_dicom_datetime() {
1270        let default_offset = FixedOffset::east_opt(0).unwrap();
1271        assert_eq!(
1272            DicomDateTime::from_date_with_time_zone(
1273                DicomDate::from_ymd(2020, 2, 29).unwrap(),
1274                default_offset
1275            ),
1276            DicomDateTime {
1277                date: DicomDate::from_ymd(2020, 2, 29).unwrap(),
1278                time: None,
1279                time_zone: Some(default_offset)
1280            }
1281        );
1282
1283        assert_eq!(
1284            DicomDateTime::from_date(DicomDate::from_ym(2020, 2).unwrap())
1285                .earliest()
1286                .unwrap(),
1287            PreciseDateTime::Naive(NaiveDateTime::new(
1288                NaiveDate::from_ymd_opt(2020, 2, 1).unwrap(),
1289                NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1290            ))
1291        );
1292
1293        assert_eq!(
1294            DicomDateTime::from_date_with_time_zone(
1295                DicomDate::from_ym(2020, 2).unwrap(),
1296                default_offset
1297            )
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, 999_999).unwrap()
1306                    ))
1307                    .unwrap()
1308            )
1309        );
1310
1311        assert_eq!(
1312            DicomDateTime::from_date_and_time_with_time_zone(
1313                DicomDate::from_ymd(2020, 2, 29).unwrap(),
1314                DicomTime::from_hmsf(23, 59, 59, 10, 2).unwrap(),
1315                default_offset
1316            )
1317            .unwrap()
1318            .earliest()
1319            .unwrap(),
1320            PreciseDateTime::TimeZone(
1321                FixedOffset::east_opt(0)
1322                    .unwrap()
1323                    .from_local_datetime(&NaiveDateTime::new(
1324                        NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1325                        NaiveTime::from_hms_micro_opt(23, 59, 59, 100_000).unwrap()
1326                    ))
1327                    .unwrap()
1328            )
1329        );
1330        assert_eq!(
1331            DicomDateTime::from_date_and_time_with_time_zone(
1332                DicomDate::from_ymd(2020, 2, 29).unwrap(),
1333                DicomTime::from_hmsf(23, 59, 59, 10, 2).unwrap(),
1334                default_offset
1335            )
1336            .unwrap()
1337            .latest()
1338            .unwrap(),
1339            PreciseDateTime::TimeZone(
1340                FixedOffset::east_opt(0)
1341                    .unwrap()
1342                    .from_local_datetime(&NaiveDateTime::new(
1343                        NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1344                        NaiveTime::from_hms_micro_opt(23, 59, 59, 109_999).unwrap()
1345                    ))
1346                    .unwrap()
1347            )
1348        );
1349
1350        assert_eq!(
1351            DicomDateTime::try_from(
1352                &FixedOffset::east_opt(0)
1353                    .unwrap()
1354                    .from_local_datetime(&NaiveDateTime::new(
1355                        NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1356                        NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1357                    ))
1358                    .unwrap()
1359            )
1360            .unwrap(),
1361            DicomDateTime {
1362                date: DicomDate::from_ymd(2020, 2, 29).unwrap(),
1363                time: Some(DicomTime::from_hms_micro(23, 59, 59, 999_999).unwrap()),
1364                time_zone: Some(default_offset)
1365            }
1366        );
1367
1368        assert_eq!(
1369            DicomDateTime::try_from(
1370                &FixedOffset::east_opt(0)
1371                    .unwrap()
1372                    .from_local_datetime(&NaiveDateTime::new(
1373                        NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1374                        NaiveTime::from_hms_micro_opt(23, 59, 59, 0).unwrap()
1375                    ))
1376                    .unwrap()
1377            )
1378            .unwrap(),
1379            DicomDateTime {
1380                date: DicomDate::from_ymd(2020, 2, 29).unwrap(),
1381                time: Some(DicomTime::from_hms_micro(23, 59, 59, 0).unwrap()),
1382                time_zone: Some(default_offset)
1383            }
1384        );
1385
1386        // leap second from chrono NaiveTime is admitted
1387        assert_eq!(
1388            DicomDateTime::try_from(
1389                &FixedOffset::east_opt(0)
1390                    .unwrap()
1391                    .from_local_datetime(&NaiveDateTime::new(
1392                        NaiveDate::from_ymd_opt(2023, 12, 31).unwrap(),
1393                        NaiveTime::from_hms_micro_opt(23, 59, 59, 1_000_000).unwrap()
1394                    ))
1395                    .unwrap()
1396            )
1397            .unwrap(),
1398            DicomDateTime {
1399                date: DicomDate::from_ymd(2023, 12, 31).unwrap(),
1400                time: Some(DicomTime::from_hms_micro(23, 59, 60, 0).unwrap()),
1401                time_zone: Some(default_offset)
1402            }
1403        );
1404
1405        assert!(matches!(
1406            DicomDateTime::from_date_with_time_zone(
1407                DicomDate::from_ymd(2021, 2, 29).unwrap(),
1408                default_offset
1409            )
1410            .earliest(),
1411            Err(crate::value::range::Error::InvalidDate { .. })
1412        ));
1413
1414        assert!(matches!(
1415            DicomDateTime::from_date_and_time_with_time_zone(
1416                DicomDate::from_ym(2020, 2).unwrap(),
1417                DicomTime::from_hms_milli(23, 59, 59, 999).unwrap(),
1418                default_offset
1419            ),
1420            Err(Error::DateTimeFromPartials {
1421                value: DateComponent::Month,
1422                ..
1423            })
1424        ));
1425        assert!(matches!(
1426            DicomDateTime::from_date_and_time_with_time_zone(
1427                DicomDate::from_y(1).unwrap(),
1428                DicomTime::from_hms_micro(23, 59, 59, 10).unwrap(),
1429                default_offset
1430            ),
1431            Err(Error::DateTimeFromPartials {
1432                value: DateComponent::Year,
1433                ..
1434            })
1435        ));
1436
1437        assert!(matches!(
1438            DicomDateTime::from_date_and_time_with_time_zone(
1439                DicomDate::from_ymd(2000, 1, 1).unwrap(),
1440                DicomTime::from_hms_milli(23, 59, 59, 10).unwrap(),
1441                default_offset
1442            )
1443            .unwrap()
1444            .exact(),
1445            Err(crate::value::range::Error::ImpreciseValue { .. })
1446        ));
1447
1448        // simple precision checks
1449        assert!(
1450            DicomDateTime::from_date_and_time(
1451                DicomDate::from_ymd(2000, 1, 1).unwrap(),
1452                DicomTime::from_hms_milli(23, 59, 59, 10).unwrap()
1453            )
1454            .unwrap()
1455            .is_precise()
1456                == false
1457        );
1458
1459        assert!(DicomDateTime::from_date_and_time(
1460            DicomDate::from_ymd(2000, 1, 1).unwrap(),
1461            DicomTime::from_hms_micro(23, 59, 59, 654_321).unwrap()
1462        )
1463        .unwrap()
1464        .is_precise());
1465    }
1466}