dicom_core/value/
range.rs

1//! Handling of date, time, date-time ranges. Needed for range matching.
2//! Parsing into ranges happens via partial precision  structures (DicomDate, DicomTime,
3//! DicomDatime) so ranges can handle null components in date, time, date-time values.
4use chrono::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
5use snafu::{Backtrace, OptionExt, ResultExt, Snafu};
6
7use crate::value::deserialize::{
8    parse_date_partial, parse_datetime_partial, parse_time_partial, Error as DeserializeError,
9};
10use crate::value::partial::{DicomDate, DicomDateTime, DicomTime, PreciseDateTime};
11
12#[derive(Debug, Snafu)]
13#[non_exhaustive]
14pub enum Error {
15    #[snafu(display("Unexpected end of element"))]
16    UnexpectedEndOfElement { backtrace: Backtrace },
17    #[snafu(display("Failed to parse value"))]
18    Parse {
19        #[snafu(backtrace)]
20        source: DeserializeError,
21    },
22    #[snafu(display("End {} is before start {}", end, start))]
23    RangeInversion {
24        start: String,
25        end: String,
26        backtrace: Backtrace,
27    },
28    #[snafu(display("No range separator present"))]
29    NoRangeSeparator { backtrace: Backtrace },
30    #[snafu(display("Date-time range can contain 1-3 '-' characters, {} were found", value))]
31    SeparatorCount { value: usize, backtrace: Backtrace },
32    #[snafu(display("Converting a time-zone naive value '{naive}' to a time-zone '{offset}' leads to invalid date-time or ambiguous results."))]
33    InvalidDateTime {
34        naive: NaiveDateTime,
35        offset: FixedOffset,
36        backtrace: Backtrace,
37    },
38    #[snafu(display(
39        "Cannot convert from an imprecise value. This value represents a date / time range"
40    ))]
41    ImpreciseValue { backtrace: Backtrace },
42    #[snafu(display("Failed to construct Date from '{y}-{m}-{d}'"))]
43    InvalidDate {
44        y: i32,
45        m: u32,
46        d: u32,
47        backtrace: Backtrace,
48    },
49    #[snafu(display("Failed to construct Time from {h}:{m}:{s}"))]
50    InvalidTime {
51        h: u32,
52        m: u32,
53        s: u32,
54        backtrace: Backtrace,
55    },
56    #[snafu(display("Failed to construct Time from {h}:{m}:{s}:{f}"))]
57    InvalidTimeMicro {
58        h: u32,
59        m: u32,
60        s: u32,
61        f: u32,
62        backtrace: Backtrace,
63    },
64    #[snafu(display("Use 'to_precise_datetime' to retrieve a precise value from a date-time"))]
65    ToPreciseDateTime { backtrace: Backtrace },
66    #[snafu(display(
67        "Parsing a date-time range from '{start}' to '{end}' with only one time-zone '{time_zone} value, second time-zone is missing.'"
68    ))]
69    AmbiguousDtRange {
70        start: NaiveDateTime,
71        end: NaiveDateTime,
72        time_zone: FixedOffset,
73        backtrace: Backtrace,
74    },
75}
76type Result<T, E = Error> = std::result::Result<T, E>;
77
78/// The DICOM protocol accepts date (DA) / time (TM) / date-time (DT) values with null components.
79///
80/// Imprecise values are to be handled as ranges.
81///
82/// This trait is implemented by date / time structures with partial precision.
83///
84/// [AsRange::is_precise()] method will check if the given value has full precision. If so, it can be
85/// converted with [AsRange::exact()] to a precise value. If not, [AsRange::range()] will yield a
86/// date / time / date-time range.
87///
88/// Please note that precision does not equal validity. A precise 'YYYYMMDD' [DicomDate] can still
89/// fail to produce a valid [chrono::NaiveDate]
90///
91/// # Examples
92///
93/// ```
94/// # use dicom_core::value::{C, PrimitiveValue};
95/// # use smallvec::smallvec;
96/// # use std::error::Error;
97/// use chrono::{NaiveDate, NaiveTime};
98/// use dicom_core::value::{AsRange, DicomDate, DicomTime, DateRange, TimeRange};
99/// # fn main() -> Result<(), Box<dyn Error>> {
100///
101/// let dicom_date = DicomDate::from_ym(2010,1)?;
102/// assert_eq!(dicom_date.is_precise(), false);
103/// assert_eq!(
104///     Some(dicom_date.earliest()?),
105///     NaiveDate::from_ymd_opt(2010,1,1)
106/// );
107/// assert_eq!(
108///     Some(dicom_date.latest()?),
109///     NaiveDate::from_ymd_opt(2010,1,31)
110/// );
111///
112/// let dicom_time = DicomTime::from_hm(10,0)?;
113/// assert_eq!(
114///     dicom_time.range()?,
115///     TimeRange::from_start_to_end(NaiveTime::from_hms(10, 0, 0),
116///         NaiveTime::from_hms_micro_opt(10, 0, 59, 999_999).unwrap())?
117/// );
118/// // only a time with 6 digits second fraction is considered precise
119/// assert!(dicom_time.exact().is_err());
120///
121/// let primitive = PrimitiveValue::from("199402");
122///
123/// // This is the fastest way to get to a useful date value, but it fails not only for invalid
124/// // dates but for imprecise ones as well.
125/// assert!(primitive.to_naive_date().is_err());
126///
127/// // Take intermediate steps:
128///
129/// // Retrieve a DicomDate.
130/// // The parser now checks for basic year and month value ranges here.
131/// // But, it would not detect invalid dates like 30th of february etc.
132/// let dicom_date : DicomDate = primitive.to_date()?;
133///
134/// // as we have a valid DicomDate value, let's check if it's precise.
135/// if dicom_date.is_precise(){
136///         // no components are missing, we can proceed by calling .exact()
137///         // which calls the `chrono` library
138///         let precise_date: NaiveDate = dicom_date.exact()?;
139/// }
140/// else{
141///         // day / month are missing, no need to call the expensive .exact() method - it will fail
142///         // retrieve the earliest possible value directly from DicomDate
143///         let earliest: NaiveDate = dicom_date.earliest()?;
144///
145///         // or convert the date to a date range instead
146///         let date_range: DateRange = dicom_date.range()?;
147///
148///         if let Some(start)  = date_range.start(){
149///             // the range has a given lower date bound
150///         }
151///
152/// }
153///
154/// # Ok(())
155/// # }
156/// ```
157pub trait AsRange {
158    type PreciseValue: PartialEq + PartialOrd;
159    type Range;
160
161    /// returns true if value has all possible date / time components
162    fn is_precise(&self) -> bool;
163
164    /// Returns a corresponding precise value, if the partial precision structure has full accuracy.
165    fn exact(&self) -> Result<Self::PreciseValue> {
166        if self.is_precise() {
167            Ok(self.earliest()?)
168        } else {
169            ImpreciseValueSnafu.fail()
170        }
171    }
172
173    /// Returns the earliest possible value from a partial precision structure.
174    /// Missing components default to 1 (days, months) or 0 (hours, minutes, ...)
175    /// If structure contains invalid combination of `DateComponent`s, it fails.
176    fn earliest(&self) -> Result<Self::PreciseValue>;
177
178    /// Returns the latest possible value from a partial precision structure.
179    /// If structure contains invalid combination of `DateComponent`s, it fails.
180    fn latest(&self) -> Result<Self::PreciseValue>;
181
182    /// Returns a tuple of the earliest and latest possible value from a partial precision structure.
183    fn range(&self) -> Result<Self::Range>;
184}
185
186impl AsRange for DicomDate {
187    type PreciseValue = NaiveDate;
188    type Range = DateRange;
189
190    fn is_precise(&self) -> bool {
191        self.day().is_some()
192    }
193
194    fn earliest(&self) -> Result<Self::PreciseValue> {
195        let (y, m, d) = {
196            (
197                *self.year() as i32,
198                *self.month().unwrap_or(&1) as u32,
199                *self.day().unwrap_or(&1) as u32,
200            )
201        };
202        NaiveDate::from_ymd_opt(y, m, d).context(InvalidDateSnafu { y, m, d })
203    }
204
205    fn latest(&self) -> Result<Self::PreciseValue> {
206        let (y, m, d) = (
207            self.year(),
208            self.month().unwrap_or(&12),
209            match self.day() {
210                Some(d) => *d as u32,
211                None => {
212                    let y = self.year();
213                    let m = self.month().unwrap_or(&12);
214                    if m == &12 {
215                        NaiveDate::from_ymd_opt(*y as i32 + 1, 1, 1).context(InvalidDateSnafu {
216                            y: *y as i32,
217                            m: 1u32,
218                            d: 1u32,
219                        })?
220                    } else {
221                        NaiveDate::from_ymd_opt(*y as i32, *m as u32 + 1, 1).context(
222                            InvalidDateSnafu {
223                                y: *y as i32,
224                                m: *m as u32,
225                                d: 1u32,
226                            },
227                        )?
228                    }
229                    .signed_duration_since(
230                        NaiveDate::from_ymd_opt(*y as i32, *m as u32, 1).context(
231                            InvalidDateSnafu {
232                                y: *y as i32,
233                                m: *m as u32,
234                                d: 1u32,
235                            },
236                        )?,
237                    )
238                    .num_days() as u32
239                }
240            },
241        );
242
243        NaiveDate::from_ymd_opt(*y as i32, *m as u32, d).context(InvalidDateSnafu {
244            y: *y as i32,
245            m: *m as u32,
246            d,
247        })
248    }
249
250    fn range(&self) -> Result<Self::Range> {
251        let start = self.earliest()?;
252        let end = self.latest()?;
253        DateRange::from_start_to_end(start, end)
254    }
255}
256
257impl AsRange for DicomTime {
258    type PreciseValue = NaiveTime;
259    type Range = TimeRange;
260
261    fn is_precise(&self) -> bool {
262        matches!(self.fraction_and_precision(), Some((_fr_, precision)) if precision == &6)
263    }
264
265    fn earliest(&self) -> Result<Self::PreciseValue> {
266        let (h, m, s, f) = (
267            self.hour(),
268            self.minute().unwrap_or(&0),
269            self.second().unwrap_or(&0),
270            match self.fraction_and_precision() {
271                None => 0,
272                Some((f, fp)) => *f * u32::pow(10, 6 - <u32>::from(*fp)),
273            },
274        );
275
276        NaiveTime::from_hms_micro_opt((*h).into(), (*m).into(), (*s).into(), f).context(
277            InvalidTimeMicroSnafu {
278                h: *h as u32,
279                m: *m as u32,
280                s: *s as u32,
281                f,
282            },
283        )
284    }
285    fn latest(&self) -> Result<Self::PreciseValue> {
286        let (h, m, s, f) = (
287            self.hour(),
288            self.minute().unwrap_or(&59),
289            self.second().unwrap_or(&59),
290            match self.fraction_and_precision() {
291                None => 999_999,
292                Some((f, fp)) => {
293                    (*f * u32::pow(10, 6 - u32::from(*fp))) + (u32::pow(10, 6 - u32::from(*fp))) - 1
294                }
295            },
296        );
297        NaiveTime::from_hms_micro_opt((*h).into(), (*m).into(), (*s).into(), f).context(
298            InvalidTimeMicroSnafu {
299                h: *h as u32,
300                m: *m as u32,
301                s: *s as u32,
302                f,
303            },
304        )
305    }
306    fn range(&self) -> Result<Self::Range> {
307        let start = self.earliest()?;
308        let end = self.latest()?;
309        TimeRange::from_start_to_end(start, end)
310    }
311}
312
313impl AsRange for DicomDateTime {
314    type PreciseValue = PreciseDateTime;
315    type Range = DateTimeRange;
316
317    fn is_precise(&self) -> bool {
318        match self.time() {
319            Some(dicom_time) => dicom_time.is_precise(),
320            None => false,
321        }
322    }
323
324    fn earliest(&self) -> Result<Self::PreciseValue> {
325        let date = self.date().earliest()?;
326        let time = match self.time() {
327            Some(time) => time.earliest()?,
328            None => NaiveTime::from_hms_opt(0, 0, 0).context(InvalidTimeSnafu {
329                h: 0u32,
330                m: 0u32,
331                s: 0u32,
332            })?,
333        };
334
335        match self.time_zone() {
336            Some(offset) => Ok(PreciseDateTime::TimeZone(
337                offset
338                    .from_local_datetime(&NaiveDateTime::new(date, time))
339                    .single()
340                    .context(InvalidDateTimeSnafu {
341                        naive: NaiveDateTime::new(date, time),
342                        offset: *offset,
343                    })?,
344            )),
345            None => Ok(PreciseDateTime::Naive(NaiveDateTime::new(date, time))),
346        }
347    }
348
349    fn latest(&self) -> Result<Self::PreciseValue> {
350        let date = self.date().latest()?;
351        let time = match self.time() {
352            Some(time) => time.latest()?,
353            None => NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).context(
354                InvalidTimeMicroSnafu {
355                    h: 23u32,
356                    m: 59u32,
357                    s: 59u32,
358                    f: 999_999u32,
359                },
360            )?,
361        };
362
363        match self.time_zone() {
364            Some(offset) => Ok(PreciseDateTime::TimeZone(
365                offset
366                    .from_local_datetime(&NaiveDateTime::new(date, time))
367                    .single()
368                    .context(InvalidDateTimeSnafu {
369                        naive: NaiveDateTime::new(date, time),
370                        offset: *offset,
371                    })?,
372            )),
373            None => Ok(PreciseDateTime::Naive(NaiveDateTime::new(date, time))),
374        }
375    }
376    fn range(&self) -> Result<Self::Range> {
377        let start = self.earliest()?;
378        let end = self.latest()?;
379
380        match (start, end) {
381            (PreciseDateTime::Naive(start), PreciseDateTime::Naive(end)) => {
382                DateTimeRange::from_start_to_end(start, end)
383            }
384            (PreciseDateTime::TimeZone(start), PreciseDateTime::TimeZone(end)) => {
385                DateTimeRange::from_start_to_end_with_time_zone(start, end)
386            }
387
388            _ => unreachable!(),
389        }
390    }
391}
392
393impl DicomDate {
394    /// Retrieves a `chrono::NaiveDate`
395    /// if the value is precise up to the day of the month.
396    pub fn to_naive_date(self) -> Result<NaiveDate> {
397        self.exact()
398    }
399}
400
401impl DicomTime {
402    /// Retrieves a `chrono::NaiveTime`
403    /// if the value is precise up to the second.
404    ///
405    /// Missing second fraction defaults to zero.
406    pub fn to_naive_time(self) -> Result<NaiveTime> {
407        if self.second().is_some() {
408            self.earliest()
409        } else {
410            ImpreciseValueSnafu.fail()
411        }
412    }
413}
414
415impl DicomDateTime {
416    /// Retrieves a [PreciseDateTime] from a date-time value.
417    /// If the date-time value is not precise or the conversion leads to ambiguous results,
418    /// it fails.
419    pub fn to_precise_datetime(&self) -> Result<PreciseDateTime> {
420        self.exact()
421    }
422
423    #[deprecated(since = "0.7.0", note = "Use `to_precise_date_time()`")]
424    pub fn to_chrono_datetime(self) -> Result<DateTime<FixedOffset>> {
425        ToPreciseDateTimeSnafu.fail()
426    }
427}
428
429/// Represents a date range as two [`Option<chrono::NaiveDate>`] values.
430/// [None] means no upper or no lower bound for range is present.
431/// # Example
432/// ```
433/// use chrono::NaiveDate;
434/// use dicom_core::value::DateRange;
435///
436/// let dr = DateRange::from_start(NaiveDate::from_ymd_opt(2000, 5, 3).unwrap());
437///
438/// assert!(dr.start().is_some());
439/// assert!(dr.end().is_none());
440/// ```
441#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
442pub struct DateRange {
443    start: Option<NaiveDate>,
444    end: Option<NaiveDate>,
445}
446/// Represents a time range as two [`Option<chrono::NaiveTime>`] values.
447/// [None] means no upper or no lower bound for range is present.
448/// # Example
449/// ```
450/// use chrono::NaiveTime;
451/// use dicom_core::value::TimeRange;
452///
453/// let tr = TimeRange::from_end(NaiveTime::from_hms_opt(10, 30, 15).unwrap());
454///
455/// assert!(tr.start().is_none());
456/// assert!(tr.end().is_some());
457/// ```
458#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
459pub struct TimeRange {
460    start: Option<NaiveTime>,
461    end: Option<NaiveTime>,
462}
463/// Represents a date-time range, that can either be time-zone naive or time-zone aware. It is stored as two [`Option<chrono::DateTime<FixedOffset>>`] or
464/// two [`Option<chrono::NaiveDateTime>`] values.
465/// [None] means no upper or no lower bound for range is present.
466///
467/// # Example
468/// ```
469/// # use std::error::Error;
470/// # fn main() -> Result<(), Box<dyn Error>> {
471/// use chrono::{NaiveDate, NaiveTime, NaiveDateTime, DateTime, FixedOffset, TimeZone};
472/// use dicom_core::value::DateTimeRange;
473///
474/// let offset = FixedOffset::west_opt(3600).unwrap();
475///
476/// let dtr = DateTimeRange::from_start_to_end_with_time_zone(
477///     offset.from_local_datetime(&NaiveDateTime::new(
478///         NaiveDate::from_ymd_opt(2000, 5, 6).unwrap(),
479///         NaiveTime::from_hms_opt(15, 0, 0).unwrap()
480///     )).unwrap(),
481///     offset.from_local_datetime(&NaiveDateTime::new(
482///         NaiveDate::from_ymd_opt(2000, 5, 6).unwrap(),
483///         NaiveTime::from_hms_opt(16, 30, 0).unwrap()
484///     )).unwrap()
485/// )?;
486///
487/// assert!(dtr.start().is_some());
488/// assert!(dtr.end().is_some());
489///  # Ok(())
490/// # }
491/// ```
492#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
493pub enum DateTimeRange {
494    /// DateTime range without time-zone information
495    Naive {
496        start: Option<NaiveDateTime>,
497        end: Option<NaiveDateTime>,
498    },
499    /// DateTime range with time-zone information
500    TimeZone {
501        start: Option<DateTime<FixedOffset>>,
502        end: Option<DateTime<FixedOffset>>,
503    },
504}
505
506impl DateRange {
507    /// Constructs a new `DateRange` from two `chrono::NaiveDate` values
508    /// monotonically ordered in time.
509    pub fn from_start_to_end(start: NaiveDate, end: NaiveDate) -> Result<DateRange> {
510        if start > end {
511            RangeInversionSnafu {
512                start: start.to_string(),
513                end: end.to_string(),
514            }
515            .fail()
516        } else {
517            Ok(DateRange {
518                start: Some(start),
519                end: Some(end),
520            })
521        }
522    }
523
524    /// Constructs a new `DateRange` beginning with a `chrono::NaiveDate` value
525    /// and no upper limit.
526    pub fn from_start(start: NaiveDate) -> DateRange {
527        DateRange {
528            start: Some(start),
529            end: None,
530        }
531    }
532
533    /// Constructs a new `DateRange` with no lower limit, ending with a `chrono::NaiveDate` value.
534    pub fn from_end(end: NaiveDate) -> DateRange {
535        DateRange {
536            start: None,
537            end: Some(end),
538        }
539    }
540
541    /// Returns a reference to lower bound of range.
542    pub fn start(&self) -> Option<&NaiveDate> {
543        self.start.as_ref()
544    }
545
546    /// Returns a reference to upper bound of range.
547    pub fn end(&self) -> Option<&NaiveDate> {
548        self.end.as_ref()
549    }
550}
551
552impl TimeRange {
553    /// Constructs a new `TimeRange` from two `chrono::NaiveTime` values
554    /// monotonically ordered in time.
555    pub fn from_start_to_end(start: NaiveTime, end: NaiveTime) -> Result<TimeRange> {
556        if start > end {
557            RangeInversionSnafu {
558                start: start.to_string(),
559                end: end.to_string(),
560            }
561            .fail()
562        } else {
563            Ok(TimeRange {
564                start: Some(start),
565                end: Some(end),
566            })
567        }
568    }
569
570    /// Constructs a new `TimeRange` beginning with a `chrono::NaiveTime` value
571    /// and no upper limit.
572    pub fn from_start(start: NaiveTime) -> TimeRange {
573        TimeRange {
574            start: Some(start),
575            end: None,
576        }
577    }
578
579    /// Constructs a new `TimeRange` with no lower limit, ending with a `chrono::NaiveTime` value.
580    pub fn from_end(end: NaiveTime) -> TimeRange {
581        TimeRange {
582            start: None,
583            end: Some(end),
584        }
585    }
586
587    /// Returns a reference to the lower bound of the range.
588    pub fn start(&self) -> Option<&NaiveTime> {
589        self.start.as_ref()
590    }
591
592    /// Returns a reference to the upper bound of the range.
593    pub fn end(&self) -> Option<&NaiveTime> {
594        self.end.as_ref()
595    }
596}
597
598impl DateTimeRange {
599    /// Constructs a new time-zone aware `DateTimeRange` from two `chrono::DateTime<FixedOffset>` values
600    /// monotonically ordered in time.
601    pub fn from_start_to_end_with_time_zone(
602        start: DateTime<FixedOffset>,
603        end: DateTime<FixedOffset>,
604    ) -> Result<DateTimeRange> {
605        if start > end {
606            RangeInversionSnafu {
607                start: start.to_string(),
608                end: end.to_string(),
609            }
610            .fail()
611        } else {
612            Ok(DateTimeRange::TimeZone {
613                start: Some(start),
614                end: Some(end),
615            })
616        }
617    }
618
619    /// Constructs a new time-zone naive `DateTimeRange` from two `chrono::NaiveDateTime` values
620    /// monotonically ordered in time.
621    pub fn from_start_to_end(start: NaiveDateTime, end: NaiveDateTime) -> Result<DateTimeRange> {
622        if start > end {
623            RangeInversionSnafu {
624                start: start.to_string(),
625                end: end.to_string(),
626            }
627            .fail()
628        } else {
629            Ok(DateTimeRange::Naive {
630                start: Some(start),
631                end: Some(end),
632            })
633        }
634    }
635
636    /// Constructs a new time-zone aware `DateTimeRange` beginning with a `chrono::DateTime<FixedOffset>` value
637    /// and no upper limit.
638    pub fn from_start_with_time_zone(start: DateTime<FixedOffset>) -> DateTimeRange {
639        DateTimeRange::TimeZone {
640            start: Some(start),
641            end: None,
642        }
643    }
644
645    /// Constructs a new time-zone naive `DateTimeRange` beginning with a `chrono::NaiveDateTime` value
646    /// and no upper limit.
647    pub fn from_start(start: NaiveDateTime) -> DateTimeRange {
648        DateTimeRange::Naive {
649            start: Some(start),
650            end: None,
651        }
652    }
653
654    /// Constructs a new time-zone aware `DateTimeRange` with no lower limit, ending with a `chrono::DateTime<FixedOffset>` value.
655    pub fn from_end_with_time_zone(end: DateTime<FixedOffset>) -> DateTimeRange {
656        DateTimeRange::TimeZone {
657            start: None,
658            end: Some(end),
659        }
660    }
661
662    /// Constructs a new time-zone naive `DateTimeRange` with no lower limit, ending with a `chrono::NaiveDateTime` value.
663    pub fn from_end(end: NaiveDateTime) -> DateTimeRange {
664        DateTimeRange::Naive {
665            start: None,
666            end: Some(end),
667        }
668    }
669
670    /// Returns the lower bound of the range, if present.
671    pub fn start(&self) -> Option<PreciseDateTime> {
672        match self {
673            DateTimeRange::Naive { start, .. } => start.map(PreciseDateTime::Naive),
674            DateTimeRange::TimeZone { start, .. } => start.map(PreciseDateTime::TimeZone),
675        }
676    }
677
678    /// Returns the upper bound of the range, if present.
679    pub fn end(&self) -> Option<PreciseDateTime> {
680        match self {
681            DateTimeRange::Naive { start: _, end } => end.map(PreciseDateTime::Naive),
682            DateTimeRange::TimeZone { start: _, end } => end.map(PreciseDateTime::TimeZone),
683        }
684    }
685
686    /// For combined datetime range matching,
687    /// this method constructs a `DateTimeRange` from a `DateRange` and a `TimeRange`.
688    /// As 'DateRange' and 'TimeRange' are always time-zone unaware, the resulting DateTimeRange
689    /// will always be time-zone unaware.
690    pub fn from_date_and_time_range(dr: DateRange, tr: TimeRange) -> Result<DateTimeRange> {
691        let start_date = dr.start();
692        let end_date = dr.end();
693
694        let start_time = *tr
695            .start()
696            .unwrap_or(&NaiveTime::from_hms_opt(0, 0, 0).context(InvalidTimeSnafu {
697                h: 0u32,
698                m: 0u32,
699                s: 0u32,
700            })?);
701        let end_time =
702            *tr.end()
703                .unwrap_or(&NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).context(
704                    InvalidTimeMicroSnafu {
705                        h: 23u32,
706                        m: 59u32,
707                        s: 59u32,
708                        f: 999_999u32,
709                    },
710                )?);
711
712        match start_date {
713            Some(sd) => match end_date {
714                Some(ed) => Ok(DateTimeRange::from_start_to_end(
715                    NaiveDateTime::new(*sd, start_time),
716                    NaiveDateTime::new(*ed, end_time),
717                )?),
718                None => Ok(DateTimeRange::from_start(NaiveDateTime::new(
719                    *sd, start_time,
720                ))),
721            },
722            None => match end_date {
723                Some(ed) => Ok(DateTimeRange::from_end(NaiveDateTime::new(*ed, end_time))),
724                None => panic!("Impossible combination of two None values for a date range."),
725            },
726        }
727    }
728}
729
730/**
731 *  Looks for a range separator '-'.
732 *  Returns a `DateRange`.
733 */
734pub fn parse_date_range(buf: &[u8]) -> Result<DateRange> {
735    // minimum length of one valid DicomDate (YYYY) and one '-' separator
736    if buf.len() < 5 {
737        return UnexpectedEndOfElementSnafu.fail();
738    }
739
740    if let Some(separator) = buf.iter().position(|e| *e == b'-') {
741        let (start, end) = buf.split_at(separator);
742        let end = &end[1..];
743        match separator {
744            0 => Ok(DateRange::from_end(
745                parse_date_partial(end).context(ParseSnafu)?.0.latest()?,
746            )),
747            i if i == buf.len() - 1 => Ok(DateRange::from_start(
748                parse_date_partial(start)
749                    .context(ParseSnafu)?
750                    .0
751                    .earliest()?,
752            )),
753            _ => Ok(DateRange::from_start_to_end(
754                parse_date_partial(start)
755                    .context(ParseSnafu)?
756                    .0
757                    .earliest()?,
758                parse_date_partial(end).context(ParseSnafu)?.0.latest()?,
759            )?),
760        }
761    } else {
762        NoRangeSeparatorSnafu.fail()
763    }
764}
765
766/// Looks for a range separator '-'.
767///  Returns a `TimeRange`.
768pub fn parse_time_range(buf: &[u8]) -> Result<TimeRange> {
769    // minimum length of one valid DicomTime (HH) and one '-' separator
770    if buf.len() < 3 {
771        return UnexpectedEndOfElementSnafu.fail();
772    }
773
774    if let Some(separator) = buf.iter().position(|e| *e == b'-') {
775        let (start, end) = buf.split_at(separator);
776        let end = &end[1..];
777        match separator {
778            0 => Ok(TimeRange::from_end(
779                parse_time_partial(end).context(ParseSnafu)?.0.latest()?,
780            )),
781            i if i == buf.len() - 1 => Ok(TimeRange::from_start(
782                parse_time_partial(start)
783                    .context(ParseSnafu)?
784                    .0
785                    .earliest()?,
786            )),
787            _ => Ok(TimeRange::from_start_to_end(
788                parse_time_partial(start)
789                    .context(ParseSnafu)?
790                    .0
791                    .earliest()?,
792                parse_time_partial(end).context(ParseSnafu)?.0.latest()?,
793            )?),
794        }
795    } else {
796        NoRangeSeparatorSnafu.fail()
797    }
798}
799
800/// The DICOM standard allows for parsing a date-time range
801/// in which one DT value provides time-zone information
802/// but the other one does not.
803/// An example of this is the value `19750101-19800101+0200`.
804///
805/// In such cases, the missing time-zone can be interpreted as the local time-zone
806/// the time-zone provided by the upper bound, or something else altogether.
807///
808/// This trait is implemented by parsers handling the aforementioned situation.
809/// For concrete implementations, see:
810/// - [`ToLocalTimeZone`] (the default implementation)
811/// - [`ToKnownTimeZone`]
812/// - [`FailOnAmbiguousRange`]
813/// - [`IgnoreTimeZone`]
814pub trait AmbiguousDtRangeParser {
815    /// Retrieve a [DateTimeRange] if the lower range bound is missing a time-zone
816    fn parse_with_ambiguous_start(
817        ambiguous_start: NaiveDateTime,
818        end: DateTime<FixedOffset>,
819    ) -> Result<DateTimeRange>;
820    /// Retrieve a [DateTimeRange] if the upper range bound is missing a time-zone
821    fn parse_with_ambiguous_end(
822        start: DateTime<FixedOffset>,
823        ambiguous_end: NaiveDateTime,
824    ) -> Result<DateTimeRange>;
825}
826
827/// For the missing time-zone,
828/// use time-zone information of the local system clock.
829/// Retrieves a [DateTimeRange::TimeZone].
830///
831/// This is the default behavior of the parser,
832/// which helps attain compliance with the standard
833/// as per [DICOM PS3.5 6.2](https://dicom.nema.org/medical/dicom/2023e/output/chtml/part05/sect_6.2.html):
834///
835/// > A Date Time Value without the optional suffix
836/// > is interpreted to be in the local time zone of the application creating the Data Element,
837/// > unless explicitly specified by the Timezone Offset From UTC (0008,0201).
838#[derive(Debug)]
839pub struct ToLocalTimeZone;
840
841/// Use time-zone information from the time-zone aware value.
842/// Retrieves a [DateTimeRange::TimeZone].
843#[derive(Debug)]
844pub struct ToKnownTimeZone;
845
846/// Fail on an attempt to parse an ambiguous date-time range.
847#[derive(Debug)]
848pub struct FailOnAmbiguousRange;
849
850/// Discard known (parsed) time-zone information.
851/// Retrieves a [DateTimeRange::Naive].
852#[derive(Debug)]
853pub struct IgnoreTimeZone;
854
855impl AmbiguousDtRangeParser for ToKnownTimeZone {
856    fn parse_with_ambiguous_start(
857        ambiguous_start: NaiveDateTime,
858        end: DateTime<FixedOffset>,
859    ) -> Result<DateTimeRange> {
860        let start = end
861            .offset()
862            .from_local_datetime(&ambiguous_start)
863            .single()
864            .context(InvalidDateTimeSnafu {
865                naive: ambiguous_start,
866                offset: *end.offset(),
867            })?;
868        if start > end {
869            RangeInversionSnafu {
870                start: ambiguous_start.to_string(),
871                end: end.to_string(),
872            }
873            .fail()
874        } else {
875            Ok(DateTimeRange::TimeZone {
876                start: Some(start),
877                end: Some(end),
878            })
879        }
880    }
881    fn parse_with_ambiguous_end(
882        start: DateTime<FixedOffset>,
883        ambiguous_end: NaiveDateTime,
884    ) -> Result<DateTimeRange> {
885        let end = start
886            .offset()
887            .from_local_datetime(&ambiguous_end)
888            .single()
889            .context(InvalidDateTimeSnafu {
890                naive: ambiguous_end,
891                offset: *start.offset(),
892            })?;
893        if start > end {
894            RangeInversionSnafu {
895                start: start.to_string(),
896                end: ambiguous_end.to_string(),
897            }
898            .fail()
899        } else {
900            Ok(DateTimeRange::TimeZone {
901                start: Some(start),
902                end: Some(end),
903            })
904        }
905    }
906}
907
908impl AmbiguousDtRangeParser for FailOnAmbiguousRange {
909    fn parse_with_ambiguous_end(
910        start: DateTime<FixedOffset>,
911        end: NaiveDateTime,
912    ) -> Result<DateTimeRange> {
913        let time_zone = *start.offset();
914        let start = start.naive_local();
915        AmbiguousDtRangeSnafu {
916            start,
917            end,
918            time_zone,
919        }
920        .fail()
921    }
922    fn parse_with_ambiguous_start(
923        start: NaiveDateTime,
924        end: DateTime<FixedOffset>,
925    ) -> Result<DateTimeRange> {
926        let time_zone = *end.offset();
927        let end = end.naive_local();
928        AmbiguousDtRangeSnafu {
929            start,
930            end,
931            time_zone,
932        }
933        .fail()
934    }
935}
936
937impl AmbiguousDtRangeParser for ToLocalTimeZone {
938    fn parse_with_ambiguous_start(
939        ambiguous_start: NaiveDateTime,
940        end: DateTime<FixedOffset>,
941    ) -> Result<DateTimeRange> {
942        let start = Local::now()
943            .offset()
944            .from_local_datetime(&ambiguous_start)
945            .single()
946            .context(InvalidDateTimeSnafu {
947                naive: ambiguous_start,
948                offset: *end.offset(),
949            })?;
950        if start > end {
951            RangeInversionSnafu {
952                start: ambiguous_start.to_string(),
953                end: end.to_string(),
954            }
955            .fail()
956        } else {
957            Ok(DateTimeRange::TimeZone {
958                start: Some(start),
959                end: Some(end),
960            })
961        }
962    }
963    fn parse_with_ambiguous_end(
964        start: DateTime<FixedOffset>,
965        ambiguous_end: NaiveDateTime,
966    ) -> Result<DateTimeRange> {
967        let end = Local::now()
968            .offset()
969            .from_local_datetime(&ambiguous_end)
970            .single()
971            .context(InvalidDateTimeSnafu {
972                naive: ambiguous_end,
973                offset: *start.offset(),
974            })?;
975        if start > end {
976            RangeInversionSnafu {
977                start: start.to_string(),
978                end: ambiguous_end.to_string(),
979            }
980            .fail()
981        } else {
982            Ok(DateTimeRange::TimeZone {
983                start: Some(start),
984                end: Some(end),
985            })
986        }
987    }
988}
989
990impl AmbiguousDtRangeParser for IgnoreTimeZone {
991    fn parse_with_ambiguous_start(
992        ambiguous_start: NaiveDateTime,
993        end: DateTime<FixedOffset>,
994    ) -> Result<DateTimeRange> {
995        let end = end.naive_local();
996        if ambiguous_start > end {
997            RangeInversionSnafu {
998                start: ambiguous_start.to_string(),
999                end: end.to_string(),
1000            }
1001            .fail()
1002        } else {
1003            Ok(DateTimeRange::Naive {
1004                start: Some(ambiguous_start),
1005                end: Some(end),
1006            })
1007        }
1008    }
1009    fn parse_with_ambiguous_end(
1010        start: DateTime<FixedOffset>,
1011        ambiguous_end: NaiveDateTime,
1012    ) -> Result<DateTimeRange> {
1013        let start = start.naive_local();
1014        if start > ambiguous_end {
1015            RangeInversionSnafu {
1016                start: start.to_string(),
1017                end: ambiguous_end.to_string(),
1018            }
1019            .fail()
1020        } else {
1021            Ok(DateTimeRange::Naive {
1022                start: Some(start),
1023                end: Some(ambiguous_end),
1024            })
1025        }
1026    }
1027}
1028
1029/// Looks for a range separator '-'.
1030/// Returns a `DateTimeRange`.
1031///
1032/// If the parser encounters two date-time values, where one is time-zone aware and the other is not,
1033/// it will use the local time-zone offset and use it instead of the missing time-zone.
1034///
1035/// This is the default behavior of the parser,
1036/// which helps attain compliance with the standard
1037/// as per [DICOM PS3.5 6.2](https://dicom.nema.org/medical/dicom/2023e/output/chtml/part05/sect_6.2.html):
1038///
1039/// > A Date Time Value without the optional suffix
1040/// > is interpreted to be in the local time zone of the application creating the Data Element,
1041/// > unless explicitly specified by the Timezone Offset From UTC (0008,0201).
1042///
1043/// To customize this behavior, please use [parse_datetime_range_custom()].
1044///
1045/// Users are advised, that for very specific inputs, inconsistent behavior can occur.
1046/// This behavior can only be produced when all of the following is true:
1047/// - two very short date-times in the form of YYYY are presented (YYYY-YYYY)
1048/// - both YYYY values can be exchanged for a valid west UTC offset, meaning year <= 1200 e.g. (1000-1100)
1049/// - only one west UTC offset is presented. e.g. (1000-1100-0100)
1050///
1051/// In such cases, two '-' characters are present and the parser will favor the first one as a range separator,
1052/// if it produces a valid `DateTimeRange`. Otherwise, it tries the second one.
1053pub fn parse_datetime_range(buf: &[u8]) -> Result<DateTimeRange> {
1054    parse_datetime_range_impl::<ToLocalTimeZone>(buf)
1055}
1056
1057/// Same as [parse_datetime_range()] but allows for custom handling of ambiguous Date-time ranges.
1058/// See [AmbiguousDtRangeParser].
1059pub fn parse_datetime_range_custom<T: AmbiguousDtRangeParser>(buf: &[u8]) -> Result<DateTimeRange> {
1060    parse_datetime_range_impl::<T>(buf)
1061}
1062
1063pub fn parse_datetime_range_impl<T: AmbiguousDtRangeParser>(buf: &[u8]) -> Result<DateTimeRange> {
1064    // minimum length of one valid DicomDateTime (YYYY) and one '-' separator
1065    if buf.len() < 5 {
1066        return UnexpectedEndOfElementSnafu.fail();
1067    }
1068    // simplest first, check for open upper and lower bound of range
1069    if buf[0] == b'-' {
1070        // starting with separator, range is None-Some
1071        let buf = &buf[1..];
1072        match parse_datetime_partial(buf).context(ParseSnafu)?.latest()? {
1073            PreciseDateTime::Naive(end) => Ok(DateTimeRange::from_end(end)),
1074            PreciseDateTime::TimeZone(end_tz) => Ok(DateTimeRange::from_end_with_time_zone(end_tz)),
1075        }
1076    } else if buf[buf.len() - 1] == b'-' {
1077        // ends with separator, range is Some-None
1078        let buf = &buf[0..(buf.len() - 1)];
1079        match parse_datetime_partial(buf)
1080            .context(ParseSnafu)?
1081            .earliest()?
1082        {
1083            PreciseDateTime::Naive(start) => Ok(DateTimeRange::from_start(start)),
1084            PreciseDateTime::TimeZone(start_tz) => {
1085                Ok(DateTimeRange::from_start_with_time_zone(start_tz))
1086            }
1087        }
1088    } else {
1089        // range must be Some-Some, now, count number of dashes and get their indexes
1090        let dashes: Vec<usize> = buf
1091            .iter()
1092            .enumerate()
1093            .filter(|(_i, c)| **c == b'-')
1094            .map(|(i, _c)| i)
1095            .collect();
1096
1097        let separator = match dashes.len() {
1098            0 => return NoRangeSeparatorSnafu.fail(), // no separator
1099            1 => dashes[0],                           // the only possible separator
1100            2 => {
1101                // there's one West UTC offset (-hhmm) in one part of the range
1102                let (start1, end1) = buf.split_at(dashes[0]);
1103
1104                let first = (
1105                    parse_datetime_partial(start1),
1106                    parse_datetime_partial(&end1[1..]),
1107                );
1108                match first {
1109                    // if split at the first dash produces a valid range, accept. Else try the other dash
1110                    (Ok(s), Ok(e)) => {
1111                        //create a result here, to check for range inversion
1112                        let dtr = match (s.earliest()?, e.latest()?) {
1113                            (PreciseDateTime::Naive(start), PreciseDateTime::Naive(end)) => {
1114                                DateTimeRange::from_start_to_end(start, end)
1115                            }
1116                            (PreciseDateTime::TimeZone(start), PreciseDateTime::TimeZone(end)) => {
1117                                DateTimeRange::from_start_to_end_with_time_zone(start, end)
1118                            }
1119                            (
1120                                // lower bound time-zone was missing
1121                                PreciseDateTime::Naive(start),
1122                                PreciseDateTime::TimeZone(end),
1123                            ) => T::parse_with_ambiguous_start(start, end),
1124                            (
1125                                PreciseDateTime::TimeZone(start),
1126                                // upper bound time-zone was missing
1127                                PreciseDateTime::Naive(end),
1128                            ) => T::parse_with_ambiguous_end(start, end),
1129                        };
1130                        match dtr {
1131                            Ok(val) => return Ok(val),
1132                            Err(_) => dashes[1],
1133                        }
1134                    }
1135                    _ => dashes[1],
1136                }
1137            }
1138            3 => dashes[1], // maximum valid count of dashes, two West UTC offsets and one separator, it's middle one
1139            len => return SeparatorCountSnafu { value: len }.fail(),
1140        };
1141
1142        let (start, end) = buf.split_at(separator);
1143        let end = &end[1..];
1144
1145        match (
1146            parse_datetime_partial(start)
1147                .context(ParseSnafu)?
1148                .earliest()?,
1149            parse_datetime_partial(end).context(ParseSnafu)?.latest()?,
1150        ) {
1151            (PreciseDateTime::Naive(start), PreciseDateTime::Naive(end)) => {
1152                DateTimeRange::from_start_to_end(start, end)
1153            }
1154            (PreciseDateTime::TimeZone(start), PreciseDateTime::TimeZone(end)) => {
1155                DateTimeRange::from_start_to_end_with_time_zone(start, end)
1156            }
1157            // lower bound time-zone was missing
1158            (PreciseDateTime::Naive(start), PreciseDateTime::TimeZone(end)) => {
1159                T::parse_with_ambiguous_start(start, end)
1160            }
1161            // upper bound time-zone was missing
1162            (PreciseDateTime::TimeZone(start), PreciseDateTime::Naive(end)) => {
1163                T::parse_with_ambiguous_end(start, end)
1164            }
1165        }
1166    }
1167}
1168
1169#[cfg(test)]
1170mod tests {
1171    use super::*;
1172
1173    #[test]
1174    fn test_date_range() {
1175        assert_eq!(
1176            DateRange::from_start(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()).start(),
1177            Some(&NaiveDate::from_ymd_opt(2020, 1, 1).unwrap())
1178        );
1179        assert_eq!(
1180            DateRange::from_end(NaiveDate::from_ymd_opt(2020, 12, 31).unwrap()).end(),
1181            Some(&NaiveDate::from_ymd_opt(2020, 12, 31).unwrap())
1182        );
1183        assert_eq!(
1184            DateRange::from_start_to_end(
1185                NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
1186                NaiveDate::from_ymd_opt(2020, 12, 31).unwrap()
1187            )
1188            .unwrap()
1189            .start(),
1190            Some(&NaiveDate::from_ymd_opt(2020, 1, 1).unwrap())
1191        );
1192        assert_eq!(
1193            DateRange::from_start_to_end(
1194                NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
1195                NaiveDate::from_ymd_opt(2020, 12, 31).unwrap()
1196            )
1197            .unwrap()
1198            .end(),
1199            Some(&NaiveDate::from_ymd_opt(2020, 12, 31).unwrap())
1200        );
1201        assert!(matches!(
1202            DateRange::from_start_to_end(
1203                NaiveDate::from_ymd_opt(2020, 12, 1).unwrap(),
1204                NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()
1205            ),
1206            Err(Error::RangeInversion {
1207                start, end ,.. }) if start == "2020-12-01" && end == "2020-01-01"
1208        ));
1209    }
1210
1211    #[test]
1212    fn test_time_range() {
1213        assert_eq!(
1214            TimeRange::from_start(NaiveTime::from_hms_opt(05, 05, 05).unwrap()).start(),
1215            Some(&NaiveTime::from_hms_opt(05, 05, 05).unwrap())
1216        );
1217        assert_eq!(
1218            TimeRange::from_end(NaiveTime::from_hms_opt(05, 05, 05).unwrap()).end(),
1219            Some(&NaiveTime::from_hms_opt(05, 05, 05).unwrap())
1220        );
1221        assert_eq!(
1222            TimeRange::from_start_to_end(
1223                NaiveTime::from_hms_opt(05, 05, 05).unwrap(),
1224                NaiveTime::from_hms_opt(05, 05, 06).unwrap()
1225            )
1226            .unwrap()
1227            .start(),
1228            Some(&NaiveTime::from_hms_opt(05, 05, 05).unwrap())
1229        );
1230        assert_eq!(
1231            TimeRange::from_start_to_end(
1232                NaiveTime::from_hms_opt(05, 05, 05).unwrap(),
1233                NaiveTime::from_hms_opt(05, 05, 06).unwrap()
1234            )
1235            .unwrap()
1236            .end(),
1237            Some(&NaiveTime::from_hms_opt(05, 05, 06).unwrap())
1238        );
1239        assert!(matches!(
1240            TimeRange::from_start_to_end(
1241                NaiveTime::from_hms_micro_opt(05, 05, 05, 123_456).unwrap(),
1242                NaiveTime::from_hms_micro_opt(05, 05, 05, 123_450).unwrap()
1243            ),
1244            Err(Error::RangeInversion {
1245                start, end ,.. }) if start == "05:05:05.123456" && end == "05:05:05.123450"
1246        ));
1247    }
1248
1249    #[test]
1250    fn test_datetime_range_with_time_zone() {
1251        let offset = FixedOffset::west_opt(3600).unwrap();
1252
1253        assert_eq!(
1254            DateTimeRange::from_start_with_time_zone(
1255                offset
1256                    .from_local_datetime(&NaiveDateTime::new(
1257                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1258                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1259                    ))
1260                    .unwrap()
1261            )
1262            .start(),
1263            Some(PreciseDateTime::TimeZone(
1264                offset
1265                    .from_local_datetime(&NaiveDateTime::new(
1266                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1267                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1268                    ))
1269                    .unwrap()
1270            ))
1271        );
1272        assert_eq!(
1273            DateTimeRange::from_end_with_time_zone(
1274                offset
1275                    .from_local_datetime(&NaiveDateTime::new(
1276                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1277                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1278                    ))
1279                    .unwrap()
1280            )
1281            .end(),
1282            Some(PreciseDateTime::TimeZone(
1283                offset
1284                    .from_local_datetime(&NaiveDateTime::new(
1285                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1286                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1287                    ))
1288                    .unwrap()
1289            ))
1290        );
1291        assert_eq!(
1292            DateTimeRange::from_start_to_end_with_time_zone(
1293                offset
1294                    .from_local_datetime(&NaiveDateTime::new(
1295                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1296                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1297                    ))
1298                    .unwrap(),
1299                offset
1300                    .from_local_datetime(&NaiveDateTime::new(
1301                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1302                        NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1303                    ))
1304                    .unwrap()
1305            )
1306            .unwrap()
1307            .start(),
1308            Some(PreciseDateTime::TimeZone(
1309                offset
1310                    .from_local_datetime(&NaiveDateTime::new(
1311                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1312                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1313                    ))
1314                    .unwrap()
1315            ))
1316        );
1317        assert_eq!(
1318            DateTimeRange::from_start_to_end_with_time_zone(
1319                offset
1320                    .from_local_datetime(&NaiveDateTime::new(
1321                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1322                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1323                    ))
1324                    .unwrap(),
1325                offset
1326                    .from_local_datetime(&NaiveDateTime::new(
1327                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1328                        NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1329                    ))
1330                    .unwrap()
1331            )
1332            .unwrap()
1333            .end(),
1334            Some(PreciseDateTime::TimeZone(
1335                offset
1336                    .from_local_datetime(&NaiveDateTime::new(
1337                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1338                        NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1339                    ))
1340                    .unwrap()
1341            ))
1342        );
1343        assert!(matches!(
1344            DateTimeRange::from_start_to_end_with_time_zone(
1345                offset
1346                .from_local_datetime(&NaiveDateTime::new(
1347                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1348                    NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1349                ))
1350                .unwrap(),
1351                offset
1352                .from_local_datetime(&NaiveDateTime::new(
1353                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1354                    NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1355                ))
1356                .unwrap()
1357            )
1358           ,
1359            Err(Error::RangeInversion {
1360                start, end ,.. })
1361                if start == "1990-01-01 01:01:01.000005 -01:00" &&
1362                   end == "1990-01-01 01:01:01.000001 -01:00"
1363        ));
1364    }
1365
1366    #[test]
1367    fn test_datetime_range_naive() {
1368        assert_eq!(
1369            DateTimeRange::from_start(NaiveDateTime::new(
1370                NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1371                NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1372            ))
1373            .start(),
1374            Some(PreciseDateTime::Naive(NaiveDateTime::new(
1375                NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1376                NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1377            )))
1378        );
1379        assert_eq!(
1380            DateTimeRange::from_end(NaiveDateTime::new(
1381                NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1382                NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1383            ))
1384            .end(),
1385            Some(PreciseDateTime::Naive(NaiveDateTime::new(
1386                NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1387                NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1388            )))
1389        );
1390        assert_eq!(
1391            DateTimeRange::from_start_to_end(
1392                NaiveDateTime::new(
1393                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1394                    NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1395                ),
1396                NaiveDateTime::new(
1397                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1398                    NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1399                )
1400            )
1401            .unwrap()
1402            .start(),
1403            Some(PreciseDateTime::Naive(NaiveDateTime::new(
1404                NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1405                NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1406            )))
1407        );
1408        assert_eq!(
1409            DateTimeRange::from_start_to_end(
1410                NaiveDateTime::new(
1411                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1412                    NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1413                ),
1414                NaiveDateTime::new(
1415                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1416                    NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1417                )
1418            )
1419            .unwrap()
1420            .end(),
1421            Some(PreciseDateTime::Naive(NaiveDateTime::new(
1422                NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1423                NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1424            )))
1425        );
1426        assert!(matches!(
1427            DateTimeRange::from_start_to_end(
1428                NaiveDateTime::new(
1429                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1430                    NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1431                ),
1432                NaiveDateTime::new(
1433                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1434                    NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1435                )
1436            )
1437           ,
1438            Err(Error::RangeInversion {
1439                start, end ,.. })
1440                if start == "1990-01-01 01:01:01.000005" &&
1441                   end == "1990-01-01 01:01:01.000001"
1442        ));
1443    }
1444
1445    #[test]
1446    fn test_parse_date_range() {
1447        assert_eq!(
1448            parse_date_range(b"-19900201").ok(),
1449            Some(DateRange {
1450                start: None,
1451                end: Some(NaiveDate::from_ymd_opt(1990, 2, 1).unwrap())
1452            })
1453        );
1454        assert_eq!(
1455            parse_date_range(b"-202002").ok(),
1456            Some(DateRange {
1457                start: None,
1458                end: Some(NaiveDate::from_ymd_opt(2020, 2, 29).unwrap())
1459            })
1460        );
1461        assert_eq!(
1462            parse_date_range(b"-0020").ok(),
1463            Some(DateRange {
1464                start: None,
1465                end: Some(NaiveDate::from_ymd_opt(20, 12, 31).unwrap())
1466            })
1467        );
1468        assert_eq!(
1469            parse_date_range(b"0002-").ok(),
1470            Some(DateRange {
1471                start: Some(NaiveDate::from_ymd_opt(2, 1, 1).unwrap()),
1472                end: None
1473            })
1474        );
1475        assert_eq!(
1476            parse_date_range(b"000203-").ok(),
1477            Some(DateRange {
1478                start: Some(NaiveDate::from_ymd_opt(2, 3, 1).unwrap()),
1479                end: None
1480            })
1481        );
1482        assert_eq!(
1483            parse_date_range(b"00020307-").ok(),
1484            Some(DateRange {
1485                start: Some(NaiveDate::from_ymd_opt(2, 3, 7).unwrap()),
1486                end: None
1487            })
1488        );
1489        assert_eq!(
1490            parse_date_range(b"0002-202002  ").ok(),
1491            Some(DateRange {
1492                start: Some(NaiveDate::from_ymd_opt(2, 1, 1).unwrap()),
1493                end: Some(NaiveDate::from_ymd_opt(2020, 2, 29).unwrap())
1494            })
1495        );
1496        assert!(parse_date_range(b"0002").is_err());
1497        assert!(parse_date_range(b"0002x").is_err());
1498        assert!(parse_date_range(b" 2010-2020").is_err());
1499    }
1500
1501    #[test]
1502    fn test_parse_time_range() {
1503        assert_eq!(
1504            parse_time_range(b"-101010.123456789").ok(),
1505            Some(TimeRange {
1506                start: None,
1507                end: Some(NaiveTime::from_hms_micro_opt(10, 10, 10, 123_456).unwrap())
1508            })
1509        );
1510        assert_eq!(
1511            parse_time_range(b"-101010.123 ").ok(),
1512            Some(TimeRange {
1513                start: None,
1514                end: Some(NaiveTime::from_hms_micro_opt(10, 10, 10, 123_999).unwrap())
1515            })
1516        );
1517        assert_eq!(
1518            parse_time_range(b"-01 ").ok(),
1519            Some(TimeRange {
1520                start: None,
1521                end: Some(NaiveTime::from_hms_micro_opt(01, 59, 59, 999_999).unwrap())
1522            })
1523        );
1524        assert_eq!(
1525            parse_time_range(b"101010.123456-").ok(),
1526            Some(TimeRange {
1527                start: Some(NaiveTime::from_hms_micro_opt(10, 10, 10, 123_456).unwrap()),
1528                end: None
1529            })
1530        );
1531        assert_eq!(
1532            parse_time_range(b"101010.123-").ok(),
1533            Some(TimeRange {
1534                start: Some(NaiveTime::from_hms_micro_opt(10, 10, 10, 123_000).unwrap()),
1535                end: None
1536            })
1537        );
1538        assert_eq!(
1539            parse_time_range(b"1010-").ok(),
1540            Some(TimeRange {
1541                start: Some(NaiveTime::from_hms_opt(10, 10, 0).unwrap()),
1542                end: None
1543            })
1544        );
1545        assert_eq!(
1546            parse_time_range(b"00-").ok(),
1547            Some(TimeRange {
1548                start: Some(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
1549                end: None
1550            })
1551        );
1552    }
1553
1554    #[test]
1555    fn test_parse_datetime_range() {
1556        assert_eq!(
1557            parse_datetime_range(b"-20200229153420.123456").ok(),
1558            Some(DateTimeRange::Naive {
1559                start: None,
1560                end: Some(NaiveDateTime::new(
1561                    NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1562                    NaiveTime::from_hms_micro_opt(15, 34, 20, 123_456).unwrap()
1563                ))
1564            })
1565        );
1566        assert_eq!(
1567            parse_datetime_range(b"-20200229153420.123").ok(),
1568            Some(DateTimeRange::Naive {
1569                start: None,
1570                end: Some(NaiveDateTime::new(
1571                    NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1572                    NaiveTime::from_hms_micro_opt(15, 34, 20, 123_999).unwrap()
1573                ))
1574            })
1575        );
1576        assert_eq!(
1577            parse_datetime_range(b"-20200229153420").ok(),
1578            Some(DateTimeRange::Naive {
1579                start: None,
1580                end: Some(NaiveDateTime::new(
1581                    NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1582                    NaiveTime::from_hms_micro_opt(15, 34, 20, 999_999).unwrap()
1583                ))
1584            })
1585        );
1586        assert_eq!(
1587            parse_datetime_range(b"-2020022915").ok(),
1588            Some(DateTimeRange::Naive {
1589                start: None,
1590                end: Some(NaiveDateTime::new(
1591                    NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1592                    NaiveTime::from_hms_micro_opt(15, 59, 59, 999_999).unwrap()
1593                ))
1594            })
1595        );
1596        assert_eq!(
1597            parse_datetime_range(b"-202002").ok(),
1598            Some(DateTimeRange::Naive {
1599                start: None,
1600                end: Some(NaiveDateTime::new(
1601                    NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1602                    NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1603                ))
1604            })
1605        );
1606        assert_eq!(
1607            parse_datetime_range(b"0002-").ok(),
1608            Some(DateTimeRange::Naive {
1609                start: Some(NaiveDateTime::new(
1610                    NaiveDate::from_ymd_opt(2, 1, 1).unwrap(),
1611                    NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1612                )),
1613                end: None
1614            })
1615        );
1616        assert_eq!(
1617            parse_datetime_range(b"00021231-").ok(),
1618            Some(DateTimeRange::Naive {
1619                start: Some(NaiveDateTime::new(
1620                    NaiveDate::from_ymd_opt(2, 12, 31).unwrap(),
1621                    NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1622                )),
1623                end: None
1624            })
1625        );
1626        // two 'east' UTC offsets get parsed
1627        assert_eq!(
1628            parse_datetime_range(b"19900101+0500-1999+1400").ok(),
1629            Some(DateTimeRange::TimeZone {
1630                start: Some(
1631                    FixedOffset::east_opt(5 * 3600)
1632                        .unwrap()
1633                        .from_local_datetime(&NaiveDateTime::new(
1634                            NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1635                            NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1636                        ))
1637                        .unwrap()
1638                ),
1639                end: Some(
1640                    FixedOffset::east_opt(14 * 3600)
1641                        .unwrap()
1642                        .from_local_datetime(&NaiveDateTime::new(
1643                            NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
1644                            NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1645                        ))
1646                        .unwrap()
1647                )
1648            })
1649        );
1650        // two 'west' Time zone offsets get parsed
1651        assert_eq!(
1652            parse_datetime_range(b"19900101-0500-1999-1200").ok(),
1653            Some(DateTimeRange::TimeZone {
1654                start: Some(
1655                    FixedOffset::west_opt(5 * 3600)
1656                        .unwrap()
1657                        .from_local_datetime(&NaiveDateTime::new(
1658                            NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1659                            NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1660                        ))
1661                        .unwrap()
1662                ),
1663                end: Some(
1664                    FixedOffset::west_opt(12 * 3600)
1665                        .unwrap()
1666                        .from_local_datetime(&NaiveDateTime::new(
1667                            NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
1668                            NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1669                        ))
1670                        .unwrap()
1671                )
1672            })
1673        );
1674        // 'east' and 'west' Time zone offsets get parsed
1675        assert_eq!(
1676            parse_datetime_range(b"19900101+1400-1999-1200").ok(),
1677            Some(DateTimeRange::TimeZone {
1678                start: Some(
1679                    FixedOffset::east_opt(14 * 3600)
1680                        .unwrap()
1681                        .from_local_datetime(&NaiveDateTime::new(
1682                            NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1683                            NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1684                        ))
1685                        .unwrap()
1686                ),
1687                end: Some(
1688                    FixedOffset::west_opt(12 * 3600)
1689                        .unwrap()
1690                        .from_local_datetime(&NaiveDateTime::new(
1691                            NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
1692                            NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1693                        ))
1694                        .unwrap()
1695                )
1696            })
1697        );
1698        // one 'west' Time zone offset gets parsed, offset cannot be mistaken for a date-time
1699        // the missing Time zone offset will be replaced with local clock time-zone offset (default behavior)
1700        assert_eq!(
1701            parse_datetime_range(b"19900101-1200-1999").unwrap(),
1702            DateTimeRange::TimeZone {
1703                start: Some(
1704                    FixedOffset::west_opt(12 * 3600)
1705                        .unwrap()
1706                        .from_local_datetime(&NaiveDateTime::new(
1707                            NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1708                            NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1709                        ))
1710                        .unwrap()
1711                ),
1712                end: Some(
1713                    Local::now()
1714                        .offset()
1715                        .from_local_datetime(&NaiveDateTime::new(
1716                            NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
1717                            NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1718                        ))
1719                        .unwrap()
1720                )
1721            }
1722        );
1723        // '0500' can either be a valid west UTC offset on the lower bound, or a valid date-time on the upper bound
1724        // Now, the first dash is considered to be a range separator, so the lower bound time-zone offset is missing
1725        // and will be considered to be the local clock time-zone offset.
1726        assert_eq!(
1727            parse_datetime_range(b"0050-0500-1000").unwrap(),
1728            DateTimeRange::TimeZone {
1729                start: Some(
1730                    Local::now()
1731                        .offset()
1732                        .from_local_datetime(&NaiveDateTime::new(
1733                            NaiveDate::from_ymd_opt(50, 1, 1).unwrap(),
1734                            NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1735                        ))
1736                        .unwrap()
1737                ),
1738                end: Some(
1739                    FixedOffset::west_opt(10 * 3600)
1740                        .unwrap()
1741                        .from_local_datetime(&NaiveDateTime::new(
1742                            NaiveDate::from_ymd_opt(500, 12, 31).unwrap(),
1743                            NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1744                        ))
1745                        .unwrap()
1746                )
1747            }
1748        );
1749        // sequence with more than 3 dashes '-' is refused.
1750        assert!(matches!(
1751            parse_datetime_range(b"0001-00021231-2021-0100-0100"),
1752            Err(Error::SeparatorCount { .. })
1753        ));
1754        // any sequence without a dash '-' is refused.
1755        assert!(matches!(
1756            parse_datetime_range(b"00021231+0500"),
1757            Err(Error::NoRangeSeparator { .. })
1758        ));
1759    }
1760}