Skip to main content

dicom_core/value/
partial.rs

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