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