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
424/// Represents a date range as two [`Option<chrono::NaiveDate>`] values.
425/// [None] means no upper or no lower bound for range is present.
426/// # Example
427/// ```
428/// use chrono::NaiveDate;
429/// use dicom_core::value::DateRange;
430///
431/// let dr = DateRange::from_start(NaiveDate::from_ymd_opt(2000, 5, 3).unwrap());
432///
433/// assert!(dr.start().is_some());
434/// assert!(dr.end().is_none());
435/// ```
436#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
437pub struct DateRange {
438    start: Option<NaiveDate>,
439    end: Option<NaiveDate>,
440}
441/// Represents a time range as two [`Option<chrono::NaiveTime>`] values.
442/// [None] means no upper or no lower bound for range is present.
443/// # Example
444/// ```
445/// use chrono::NaiveTime;
446/// use dicom_core::value::TimeRange;
447///
448/// let tr = TimeRange::from_end(NaiveTime::from_hms_opt(10, 30, 15).unwrap());
449///
450/// assert!(tr.start().is_none());
451/// assert!(tr.end().is_some());
452/// ```
453#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
454pub struct TimeRange {
455    start: Option<NaiveTime>,
456    end: Option<NaiveTime>,
457}
458/// 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
459/// two [`Option<chrono::NaiveDateTime>`] values.
460/// [None] means no upper or no lower bound for range is present.
461///
462/// # Example
463/// ```
464/// # use std::error::Error;
465/// # fn main() -> Result<(), Box<dyn Error>> {
466/// use chrono::{NaiveDate, NaiveTime, NaiveDateTime, DateTime, FixedOffset, TimeZone};
467/// use dicom_core::value::DateTimeRange;
468///
469/// let offset = FixedOffset::west_opt(3600).unwrap();
470///
471/// let dtr = DateTimeRange::from_start_to_end_with_time_zone(
472///     offset.from_local_datetime(&NaiveDateTime::new(
473///         NaiveDate::from_ymd_opt(2000, 5, 6).unwrap(),
474///         NaiveTime::from_hms_opt(15, 0, 0).unwrap()
475///     )).unwrap(),
476///     offset.from_local_datetime(&NaiveDateTime::new(
477///         NaiveDate::from_ymd_opt(2000, 5, 6).unwrap(),
478///         NaiveTime::from_hms_opt(16, 30, 0).unwrap()
479///     )).unwrap()
480/// )?;
481///
482/// assert!(dtr.start().is_some());
483/// assert!(dtr.end().is_some());
484///  # Ok(())
485/// # }
486/// ```
487#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
488pub enum DateTimeRange {
489    /// DateTime range without time-zone information
490    Naive {
491        start: Option<NaiveDateTime>,
492        end: Option<NaiveDateTime>,
493    },
494    /// DateTime range with time-zone information
495    TimeZone {
496        start: Option<DateTime<FixedOffset>>,
497        end: Option<DateTime<FixedOffset>>,
498    },
499}
500
501impl DateRange {
502    /// Constructs a new `DateRange` from two `chrono::NaiveDate` values
503    /// monotonically ordered in time.
504    pub fn from_start_to_end(start: NaiveDate, end: NaiveDate) -> Result<DateRange> {
505        if start > end {
506            RangeInversionSnafu {
507                start: start.to_string(),
508                end: end.to_string(),
509            }
510            .fail()
511        } else {
512            Ok(DateRange {
513                start: Some(start),
514                end: Some(end),
515            })
516        }
517    }
518
519    /// Constructs a new `DateRange` beginning with a `chrono::NaiveDate` value
520    /// and no upper limit.
521    pub fn from_start(start: NaiveDate) -> DateRange {
522        DateRange {
523            start: Some(start),
524            end: None,
525        }
526    }
527
528    /// Constructs a new `DateRange` with no lower limit, ending with a `chrono::NaiveDate` value.
529    pub fn from_end(end: NaiveDate) -> DateRange {
530        DateRange {
531            start: None,
532            end: Some(end),
533        }
534    }
535
536    /// Returns a reference to lower bound of range.
537    pub fn start(&self) -> Option<&NaiveDate> {
538        self.start.as_ref()
539    }
540
541    /// Returns a reference to upper bound of range.
542    pub fn end(&self) -> Option<&NaiveDate> {
543        self.end.as_ref()
544    }
545}
546
547impl TimeRange {
548    /// Constructs a new `TimeRange` from two `chrono::NaiveTime` values
549    /// monotonically ordered in time.
550    pub fn from_start_to_end(start: NaiveTime, end: NaiveTime) -> Result<TimeRange> {
551        if start > end {
552            RangeInversionSnafu {
553                start: start.to_string(),
554                end: end.to_string(),
555            }
556            .fail()
557        } else {
558            Ok(TimeRange {
559                start: Some(start),
560                end: Some(end),
561            })
562        }
563    }
564
565    /// Constructs a new `TimeRange` beginning with a `chrono::NaiveTime` value
566    /// and no upper limit.
567    pub fn from_start(start: NaiveTime) -> TimeRange {
568        TimeRange {
569            start: Some(start),
570            end: None,
571        }
572    }
573
574    /// Constructs a new `TimeRange` with no lower limit, ending with a `chrono::NaiveTime` value.
575    pub fn from_end(end: NaiveTime) -> TimeRange {
576        TimeRange {
577            start: None,
578            end: Some(end),
579        }
580    }
581
582    /// Returns a reference to the lower bound of the range.
583    pub fn start(&self) -> Option<&NaiveTime> {
584        self.start.as_ref()
585    }
586
587    /// Returns a reference to the upper bound of the range.
588    pub fn end(&self) -> Option<&NaiveTime> {
589        self.end.as_ref()
590    }
591}
592
593impl DateTimeRange {
594    /// Constructs a new time-zone aware `DateTimeRange` from two `chrono::DateTime<FixedOffset>` values
595    /// monotonically ordered in time.
596    pub fn from_start_to_end_with_time_zone(
597        start: DateTime<FixedOffset>,
598        end: DateTime<FixedOffset>,
599    ) -> Result<DateTimeRange> {
600        if start > end {
601            RangeInversionSnafu {
602                start: start.to_string(),
603                end: end.to_string(),
604            }
605            .fail()
606        } else {
607            Ok(DateTimeRange::TimeZone {
608                start: Some(start),
609                end: Some(end),
610            })
611        }
612    }
613
614    /// Constructs a new time-zone naive `DateTimeRange` from two `chrono::NaiveDateTime` values
615    /// monotonically ordered in time.
616    pub fn from_start_to_end(start: NaiveDateTime, end: NaiveDateTime) -> Result<DateTimeRange> {
617        if start > end {
618            RangeInversionSnafu {
619                start: start.to_string(),
620                end: end.to_string(),
621            }
622            .fail()
623        } else {
624            Ok(DateTimeRange::Naive {
625                start: Some(start),
626                end: Some(end),
627            })
628        }
629    }
630
631    /// Constructs a new time-zone aware `DateTimeRange` beginning with a `chrono::DateTime<FixedOffset>` value
632    /// and no upper limit.
633    pub fn from_start_with_time_zone(start: DateTime<FixedOffset>) -> DateTimeRange {
634        DateTimeRange::TimeZone {
635            start: Some(start),
636            end: None,
637        }
638    }
639
640    /// Constructs a new time-zone naive `DateTimeRange` beginning with a `chrono::NaiveDateTime` value
641    /// and no upper limit.
642    pub fn from_start(start: NaiveDateTime) -> DateTimeRange {
643        DateTimeRange::Naive {
644            start: Some(start),
645            end: None,
646        }
647    }
648
649    /// Constructs a new time-zone aware `DateTimeRange` with no lower limit, ending with a `chrono::DateTime<FixedOffset>` value.
650    pub fn from_end_with_time_zone(end: DateTime<FixedOffset>) -> DateTimeRange {
651        DateTimeRange::TimeZone {
652            start: None,
653            end: Some(end),
654        }
655    }
656
657    /// Constructs a new time-zone naive `DateTimeRange` with no lower limit, ending with a `chrono::NaiveDateTime` value.
658    pub fn from_end(end: NaiveDateTime) -> DateTimeRange {
659        DateTimeRange::Naive {
660            start: None,
661            end: Some(end),
662        }
663    }
664
665    /// Returns the lower bound of the range, if present.
666    pub fn start(&self) -> Option<PreciseDateTime> {
667        match self {
668            DateTimeRange::Naive { start, .. } => start.map(PreciseDateTime::Naive),
669            DateTimeRange::TimeZone { start, .. } => start.map(PreciseDateTime::TimeZone),
670        }
671    }
672
673    /// Returns the upper bound of the range, if present.
674    pub fn end(&self) -> Option<PreciseDateTime> {
675        match self {
676            DateTimeRange::Naive { start: _, end } => end.map(PreciseDateTime::Naive),
677            DateTimeRange::TimeZone { start: _, end } => end.map(PreciseDateTime::TimeZone),
678        }
679    }
680
681    /// For combined datetime range matching,
682    /// this method constructs a `DateTimeRange` from a `DateRange` and a `TimeRange`.
683    /// As 'DateRange' and 'TimeRange' are always time-zone unaware, the resulting DateTimeRange
684    /// will always be time-zone unaware.
685    pub fn from_date_and_time_range(dr: DateRange, tr: TimeRange) -> Result<DateTimeRange> {
686        let start_date = dr.start();
687        let end_date = dr.end();
688
689        let start_time = *tr
690            .start()
691            .unwrap_or(&NaiveTime::from_hms_opt(0, 0, 0).context(InvalidTimeSnafu {
692                h: 0u32,
693                m: 0u32,
694                s: 0u32,
695            })?);
696        let end_time =
697            *tr.end()
698                .unwrap_or(&NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).context(
699                    InvalidTimeMicroSnafu {
700                        h: 23u32,
701                        m: 59u32,
702                        s: 59u32,
703                        f: 999_999u32,
704                    },
705                )?);
706
707        match start_date {
708            Some(sd) => match end_date {
709                Some(ed) => Ok(DateTimeRange::from_start_to_end(
710                    NaiveDateTime::new(*sd, start_time),
711                    NaiveDateTime::new(*ed, end_time),
712                )?),
713                None => Ok(DateTimeRange::from_start(NaiveDateTime::new(
714                    *sd, start_time,
715                ))),
716            },
717            None => match end_date {
718                Some(ed) => Ok(DateTimeRange::from_end(NaiveDateTime::new(*ed, end_time))),
719                None => panic!("Impossible combination of two None values for a date range."),
720            },
721        }
722    }
723}
724
725/**
726 *  Looks for a range separator '-'.
727 *  Returns a `DateRange`.
728 */
729pub fn parse_date_range(buf: &[u8]) -> Result<DateRange> {
730    // minimum length of one valid DicomDate (YYYY) and one '-' separator
731    if buf.len() < 5 {
732        return UnexpectedEndOfElementSnafu.fail();
733    }
734
735    if let Some(separator) = buf.iter().position(|e| *e == b'-') {
736        let (start, end) = buf.split_at(separator);
737        let end = &end[1..];
738        match separator {
739            0 => Ok(DateRange::from_end(
740                parse_date_partial(end).context(ParseSnafu)?.0.latest()?,
741            )),
742            i if i == buf.len() - 1 => Ok(DateRange::from_start(
743                parse_date_partial(start)
744                    .context(ParseSnafu)?
745                    .0
746                    .earliest()?,
747            )),
748            _ => Ok(DateRange::from_start_to_end(
749                parse_date_partial(start)
750                    .context(ParseSnafu)?
751                    .0
752                    .earliest()?,
753                parse_date_partial(end).context(ParseSnafu)?.0.latest()?,
754            )?),
755        }
756    } else {
757        NoRangeSeparatorSnafu.fail()
758    }
759}
760
761/// Looks for a range separator '-'.
762///  Returns a `TimeRange`.
763pub fn parse_time_range(buf: &[u8]) -> Result<TimeRange> {
764    // minimum length of one valid DicomTime (HH) and one '-' separator
765    if buf.len() < 3 {
766        return UnexpectedEndOfElementSnafu.fail();
767    }
768
769    if let Some(separator) = buf.iter().position(|e| *e == b'-') {
770        let (start, end) = buf.split_at(separator);
771        let end = &end[1..];
772        match separator {
773            0 => Ok(TimeRange::from_end(
774                parse_time_partial(end).context(ParseSnafu)?.0.latest()?,
775            )),
776            i if i == buf.len() - 1 => Ok(TimeRange::from_start(
777                parse_time_partial(start)
778                    .context(ParseSnafu)?
779                    .0
780                    .earliest()?,
781            )),
782            _ => Ok(TimeRange::from_start_to_end(
783                parse_time_partial(start)
784                    .context(ParseSnafu)?
785                    .0
786                    .earliest()?,
787                parse_time_partial(end).context(ParseSnafu)?.0.latest()?,
788            )?),
789        }
790    } else {
791        NoRangeSeparatorSnafu.fail()
792    }
793}
794
795/// The DICOM standard allows for parsing a date-time range
796/// in which one DT value provides time-zone information
797/// but the other one does not.
798/// An example of this is the value `19750101-19800101+0200`.
799///
800/// In such cases, the missing time-zone can be interpreted as the local time-zone
801/// the time-zone provided by the upper bound, or something else altogether.
802///
803/// This trait is implemented by parsers handling the aforementioned situation.
804/// For concrete implementations, see:
805/// - [`ToLocalTimeZone`] (the default implementation)
806/// - [`ToKnownTimeZone`]
807/// - [`FailOnAmbiguousRange`]
808/// - [`IgnoreTimeZone`]
809pub trait AmbiguousDtRangeParser {
810    /// Retrieve a [DateTimeRange] if the lower range bound is missing a time-zone
811    fn parse_with_ambiguous_start(
812        ambiguous_start: NaiveDateTime,
813        end: DateTime<FixedOffset>,
814    ) -> Result<DateTimeRange>;
815    /// Retrieve a [DateTimeRange] if the upper range bound is missing a time-zone
816    fn parse_with_ambiguous_end(
817        start: DateTime<FixedOffset>,
818        ambiguous_end: NaiveDateTime,
819    ) -> Result<DateTimeRange>;
820}
821
822/// For the missing time-zone,
823/// use time-zone information of the local system clock.
824/// Retrieves a [DateTimeRange::TimeZone].
825///
826/// This is the default behavior of the parser,
827/// which helps attain compliance with the standard
828/// as per [DICOM PS3.5 6.2](https://dicom.nema.org/medical/dicom/2023e/output/chtml/part05/sect_6.2.html):
829///
830/// > A Date Time Value without the optional suffix
831/// > is interpreted to be in the local time zone of the application creating the Data Element,
832/// > unless explicitly specified by the Timezone Offset From UTC (0008,0201).
833#[derive(Debug)]
834pub struct ToLocalTimeZone;
835
836/// Use time-zone information from the time-zone aware value.
837/// Retrieves a [DateTimeRange::TimeZone].
838#[derive(Debug)]
839pub struct ToKnownTimeZone;
840
841/// Fail on an attempt to parse an ambiguous date-time range.
842#[derive(Debug)]
843pub struct FailOnAmbiguousRange;
844
845/// Discard known (parsed) time-zone information.
846/// Retrieves a [DateTimeRange::Naive].
847#[derive(Debug)]
848pub struct IgnoreTimeZone;
849
850impl AmbiguousDtRangeParser for ToKnownTimeZone {
851    fn parse_with_ambiguous_start(
852        ambiguous_start: NaiveDateTime,
853        end: DateTime<FixedOffset>,
854    ) -> Result<DateTimeRange> {
855        let start = end
856            .offset()
857            .from_local_datetime(&ambiguous_start)
858            .single()
859            .context(InvalidDateTimeSnafu {
860                naive: ambiguous_start,
861                offset: *end.offset(),
862            })?;
863        if start > end {
864            RangeInversionSnafu {
865                start: ambiguous_start.to_string(),
866                end: end.to_string(),
867            }
868            .fail()
869        } else {
870            Ok(DateTimeRange::TimeZone {
871                start: Some(start),
872                end: Some(end),
873            })
874        }
875    }
876    fn parse_with_ambiguous_end(
877        start: DateTime<FixedOffset>,
878        ambiguous_end: NaiveDateTime,
879    ) -> Result<DateTimeRange> {
880        let end = start
881            .offset()
882            .from_local_datetime(&ambiguous_end)
883            .single()
884            .context(InvalidDateTimeSnafu {
885                naive: ambiguous_end,
886                offset: *start.offset(),
887            })?;
888        if start > end {
889            RangeInversionSnafu {
890                start: start.to_string(),
891                end: ambiguous_end.to_string(),
892            }
893            .fail()
894        } else {
895            Ok(DateTimeRange::TimeZone {
896                start: Some(start),
897                end: Some(end),
898            })
899        }
900    }
901}
902
903impl AmbiguousDtRangeParser for FailOnAmbiguousRange {
904    fn parse_with_ambiguous_end(
905        start: DateTime<FixedOffset>,
906        end: NaiveDateTime,
907    ) -> Result<DateTimeRange> {
908        let time_zone = *start.offset();
909        let start = start.naive_local();
910        AmbiguousDtRangeSnafu {
911            start,
912            end,
913            time_zone,
914        }
915        .fail()
916    }
917    fn parse_with_ambiguous_start(
918        start: NaiveDateTime,
919        end: DateTime<FixedOffset>,
920    ) -> Result<DateTimeRange> {
921        let time_zone = *end.offset();
922        let end = end.naive_local();
923        AmbiguousDtRangeSnafu {
924            start,
925            end,
926            time_zone,
927        }
928        .fail()
929    }
930}
931
932impl AmbiguousDtRangeParser for ToLocalTimeZone {
933    fn parse_with_ambiguous_start(
934        ambiguous_start: NaiveDateTime,
935        end: DateTime<FixedOffset>,
936    ) -> Result<DateTimeRange> {
937        let start = Local::now()
938            .offset()
939            .from_local_datetime(&ambiguous_start)
940            .single()
941            .context(InvalidDateTimeSnafu {
942                naive: ambiguous_start,
943                offset: *end.offset(),
944            })?;
945        if start > end {
946            RangeInversionSnafu {
947                start: ambiguous_start.to_string(),
948                end: end.to_string(),
949            }
950            .fail()
951        } else {
952            Ok(DateTimeRange::TimeZone {
953                start: Some(start),
954                end: Some(end),
955            })
956        }
957    }
958    fn parse_with_ambiguous_end(
959        start: DateTime<FixedOffset>,
960        ambiguous_end: NaiveDateTime,
961    ) -> Result<DateTimeRange> {
962        let end = Local::now()
963            .offset()
964            .from_local_datetime(&ambiguous_end)
965            .single()
966            .context(InvalidDateTimeSnafu {
967                naive: ambiguous_end,
968                offset: *start.offset(),
969            })?;
970        if start > end {
971            RangeInversionSnafu {
972                start: start.to_string(),
973                end: ambiguous_end.to_string(),
974            }
975            .fail()
976        } else {
977            Ok(DateTimeRange::TimeZone {
978                start: Some(start),
979                end: Some(end),
980            })
981        }
982    }
983}
984
985impl AmbiguousDtRangeParser for IgnoreTimeZone {
986    fn parse_with_ambiguous_start(
987        ambiguous_start: NaiveDateTime,
988        end: DateTime<FixedOffset>,
989    ) -> Result<DateTimeRange> {
990        let end = end.naive_local();
991        if ambiguous_start > end {
992            RangeInversionSnafu {
993                start: ambiguous_start.to_string(),
994                end: end.to_string(),
995            }
996            .fail()
997        } else {
998            Ok(DateTimeRange::Naive {
999                start: Some(ambiguous_start),
1000                end: Some(end),
1001            })
1002        }
1003    }
1004    fn parse_with_ambiguous_end(
1005        start: DateTime<FixedOffset>,
1006        ambiguous_end: NaiveDateTime,
1007    ) -> Result<DateTimeRange> {
1008        let start = start.naive_local();
1009        if start > ambiguous_end {
1010            RangeInversionSnafu {
1011                start: start.to_string(),
1012                end: ambiguous_end.to_string(),
1013            }
1014            .fail()
1015        } else {
1016            Ok(DateTimeRange::Naive {
1017                start: Some(start),
1018                end: Some(ambiguous_end),
1019            })
1020        }
1021    }
1022}
1023
1024/// Looks for a range separator '-'.
1025/// Returns a `DateTimeRange`.
1026///
1027/// If the parser encounters two date-time values, where one is time-zone aware and the other is not,
1028/// it will use the local time-zone offset and use it instead of the missing time-zone.
1029///
1030/// This is the default behavior of the parser,
1031/// which helps attain compliance with the standard
1032/// as per [DICOM PS3.5 6.2](https://dicom.nema.org/medical/dicom/2023e/output/chtml/part05/sect_6.2.html):
1033///
1034/// > A Date Time Value without the optional suffix
1035/// > is interpreted to be in the local time zone of the application creating the Data Element,
1036/// > unless explicitly specified by the Timezone Offset From UTC (0008,0201).
1037///
1038/// To customize this behavior, please use [parse_datetime_range_custom()].
1039///
1040/// Users are advised, that for very specific inputs, inconsistent behavior can occur.
1041/// This behavior can only be produced when all of the following is true:
1042/// - two very short date-times in the form of YYYY are presented (YYYY-YYYY)
1043/// - both YYYY values can be exchanged for a valid west UTC offset, meaning year <= 1200 e.g. (1000-1100)
1044/// - only one west UTC offset is presented. e.g. (1000-1100-0100)
1045///
1046/// In such cases, two '-' characters are present and the parser will favor the first one as a range separator,
1047/// if it produces a valid `DateTimeRange`. Otherwise, it tries the second one.
1048pub fn parse_datetime_range(buf: &[u8]) -> Result<DateTimeRange> {
1049    parse_datetime_range_impl::<ToLocalTimeZone>(buf)
1050}
1051
1052/// Same as [parse_datetime_range()] but allows for custom handling of ambiguous Date-time ranges.
1053/// See [AmbiguousDtRangeParser].
1054pub fn parse_datetime_range_custom<T: AmbiguousDtRangeParser>(buf: &[u8]) -> Result<DateTimeRange> {
1055    parse_datetime_range_impl::<T>(buf)
1056}
1057
1058pub fn parse_datetime_range_impl<T: AmbiguousDtRangeParser>(buf: &[u8]) -> Result<DateTimeRange> {
1059    // minimum length of one valid DicomDateTime (YYYY) and one '-' separator
1060    if buf.len() < 5 {
1061        return UnexpectedEndOfElementSnafu.fail();
1062    }
1063    // simplest first, check for open upper and lower bound of range
1064    if buf[0] == b'-' {
1065        // starting with separator, range is None-Some
1066        let buf = &buf[1..];
1067        match parse_datetime_partial(buf).context(ParseSnafu)?.latest()? {
1068            PreciseDateTime::Naive(end) => Ok(DateTimeRange::from_end(end)),
1069            PreciseDateTime::TimeZone(end_tz) => Ok(DateTimeRange::from_end_with_time_zone(end_tz)),
1070        }
1071    } else if buf[buf.len() - 1] == b'-' {
1072        // ends with separator, range is Some-None
1073        let buf = &buf[0..(buf.len() - 1)];
1074        match parse_datetime_partial(buf)
1075            .context(ParseSnafu)?
1076            .earliest()?
1077        {
1078            PreciseDateTime::Naive(start) => Ok(DateTimeRange::from_start(start)),
1079            PreciseDateTime::TimeZone(start_tz) => {
1080                Ok(DateTimeRange::from_start_with_time_zone(start_tz))
1081            }
1082        }
1083    } else {
1084        // range must be Some-Some, now, count number of dashes and get their indexes
1085        let dashes: Vec<usize> = buf
1086            .iter()
1087            .enumerate()
1088            .filter(|(_i, c)| **c == b'-')
1089            .map(|(i, _c)| i)
1090            .collect();
1091
1092        let separator = match dashes.len() {
1093            0 => return NoRangeSeparatorSnafu.fail(), // no separator
1094            1 => dashes[0],                           // the only possible separator
1095            2 => {
1096                // there's one West UTC offset (-hhmm) in one part of the range
1097                let (start1, end1) = buf.split_at(dashes[0]);
1098
1099                let first = (
1100                    parse_datetime_partial(start1),
1101                    parse_datetime_partial(&end1[1..]),
1102                );
1103                match first {
1104                    // if split at the first dash produces a valid range, accept. Else try the other dash
1105                    (Ok(s), Ok(e)) => {
1106                        //create a result here, to check for range inversion
1107                        let dtr = match (s.earliest()?, e.latest()?) {
1108                            (PreciseDateTime::Naive(start), PreciseDateTime::Naive(end)) => {
1109                                DateTimeRange::from_start_to_end(start, end)
1110                            }
1111                            (PreciseDateTime::TimeZone(start), PreciseDateTime::TimeZone(end)) => {
1112                                DateTimeRange::from_start_to_end_with_time_zone(start, end)
1113                            }
1114                            (
1115                                // lower bound time-zone was missing
1116                                PreciseDateTime::Naive(start),
1117                                PreciseDateTime::TimeZone(end),
1118                            ) => T::parse_with_ambiguous_start(start, end),
1119                            (
1120                                PreciseDateTime::TimeZone(start),
1121                                // upper bound time-zone was missing
1122                                PreciseDateTime::Naive(end),
1123                            ) => T::parse_with_ambiguous_end(start, end),
1124                        };
1125                        match dtr {
1126                            Ok(val) => return Ok(val),
1127                            Err(_) => dashes[1],
1128                        }
1129                    }
1130                    _ => dashes[1],
1131                }
1132            }
1133            3 => dashes[1], // maximum valid count of dashes, two West UTC offsets and one separator, it's middle one
1134            len => return SeparatorCountSnafu { value: len }.fail(),
1135        };
1136
1137        let (start, end) = buf.split_at(separator);
1138        let end = &end[1..];
1139
1140        match (
1141            parse_datetime_partial(start)
1142                .context(ParseSnafu)?
1143                .earliest()?,
1144            parse_datetime_partial(end).context(ParseSnafu)?.latest()?,
1145        ) {
1146            (PreciseDateTime::Naive(start), PreciseDateTime::Naive(end)) => {
1147                DateTimeRange::from_start_to_end(start, end)
1148            }
1149            (PreciseDateTime::TimeZone(start), PreciseDateTime::TimeZone(end)) => {
1150                DateTimeRange::from_start_to_end_with_time_zone(start, end)
1151            }
1152            // lower bound time-zone was missing
1153            (PreciseDateTime::Naive(start), PreciseDateTime::TimeZone(end)) => {
1154                T::parse_with_ambiguous_start(start, end)
1155            }
1156            // upper bound time-zone was missing
1157            (PreciseDateTime::TimeZone(start), PreciseDateTime::Naive(end)) => {
1158                T::parse_with_ambiguous_end(start, end)
1159            }
1160        }
1161    }
1162}
1163
1164#[cfg(test)]
1165mod tests {
1166    use super::*;
1167
1168    #[test]
1169    fn test_date_range() {
1170        assert_eq!(
1171            DateRange::from_start(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()).start(),
1172            Some(&NaiveDate::from_ymd_opt(2020, 1, 1).unwrap())
1173        );
1174        assert_eq!(
1175            DateRange::from_end(NaiveDate::from_ymd_opt(2020, 12, 31).unwrap()).end(),
1176            Some(&NaiveDate::from_ymd_opt(2020, 12, 31).unwrap())
1177        );
1178        assert_eq!(
1179            DateRange::from_start_to_end(
1180                NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
1181                NaiveDate::from_ymd_opt(2020, 12, 31).unwrap()
1182            )
1183            .unwrap()
1184            .start(),
1185            Some(&NaiveDate::from_ymd_opt(2020, 1, 1).unwrap())
1186        );
1187        assert_eq!(
1188            DateRange::from_start_to_end(
1189                NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
1190                NaiveDate::from_ymd_opt(2020, 12, 31).unwrap()
1191            )
1192            .unwrap()
1193            .end(),
1194            Some(&NaiveDate::from_ymd_opt(2020, 12, 31).unwrap())
1195        );
1196        assert!(matches!(
1197            DateRange::from_start_to_end(
1198                NaiveDate::from_ymd_opt(2020, 12, 1).unwrap(),
1199                NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()
1200            ),
1201            Err(Error::RangeInversion {
1202                start, end ,.. }) if start == "2020-12-01" && end == "2020-01-01"
1203        ));
1204    }
1205
1206    #[test]
1207    fn test_time_range() {
1208        assert_eq!(
1209            TimeRange::from_start(NaiveTime::from_hms_opt(5, 5, 5).unwrap()).start(),
1210            Some(&NaiveTime::from_hms_opt(5, 5, 5).unwrap())
1211        );
1212        assert_eq!(
1213            TimeRange::from_end(NaiveTime::from_hms_opt(5, 5, 5).unwrap()).end(),
1214            Some(&NaiveTime::from_hms_opt(5, 5, 5).unwrap())
1215        );
1216        assert_eq!(
1217            TimeRange::from_start_to_end(
1218                NaiveTime::from_hms_opt(5, 5, 5).unwrap(),
1219                NaiveTime::from_hms_opt(5, 5, 6).unwrap()
1220            )
1221            .unwrap()
1222            .start(),
1223            Some(&NaiveTime::from_hms_opt(5, 5, 5).unwrap())
1224        );
1225        assert_eq!(
1226            TimeRange::from_start_to_end(
1227                NaiveTime::from_hms_opt(5, 5, 5).unwrap(),
1228                NaiveTime::from_hms_opt(5, 5, 6).unwrap()
1229            )
1230            .unwrap()
1231            .end(),
1232            Some(&NaiveTime::from_hms_opt(5, 5, 6).unwrap())
1233        );
1234        assert!(matches!(
1235            TimeRange::from_start_to_end(
1236                NaiveTime::from_hms_micro_opt(5, 5, 5, 123_456).unwrap(),
1237                NaiveTime::from_hms_micro_opt(5, 5, 5, 123_450).unwrap()
1238            ),
1239            Err(Error::RangeInversion {
1240                start, end ,.. }) if start == "05:05:05.123456" && end == "05:05:05.123450"
1241        ));
1242    }
1243
1244    #[test]
1245    fn test_datetime_range_with_time_zone() {
1246        let offset = FixedOffset::west_opt(3600).unwrap();
1247
1248        assert_eq!(
1249            DateTimeRange::from_start_with_time_zone(
1250                offset
1251                    .from_local_datetime(&NaiveDateTime::new(
1252                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1253                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1254                    ))
1255                    .unwrap()
1256            )
1257            .start(),
1258            Some(PreciseDateTime::TimeZone(
1259                offset
1260                    .from_local_datetime(&NaiveDateTime::new(
1261                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1262                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1263                    ))
1264                    .unwrap()
1265            ))
1266        );
1267        assert_eq!(
1268            DateTimeRange::from_end_with_time_zone(
1269                offset
1270                    .from_local_datetime(&NaiveDateTime::new(
1271                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1272                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1273                    ))
1274                    .unwrap()
1275            )
1276            .end(),
1277            Some(PreciseDateTime::TimeZone(
1278                offset
1279                    .from_local_datetime(&NaiveDateTime::new(
1280                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1281                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1282                    ))
1283                    .unwrap()
1284            ))
1285        );
1286        assert_eq!(
1287            DateTimeRange::from_start_to_end_with_time_zone(
1288                offset
1289                    .from_local_datetime(&NaiveDateTime::new(
1290                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1291                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1292                    ))
1293                    .unwrap(),
1294                offset
1295                    .from_local_datetime(&NaiveDateTime::new(
1296                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1297                        NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1298                    ))
1299                    .unwrap()
1300            )
1301            .unwrap()
1302            .start(),
1303            Some(PreciseDateTime::TimeZone(
1304                offset
1305                    .from_local_datetime(&NaiveDateTime::new(
1306                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1307                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1308                    ))
1309                    .unwrap()
1310            ))
1311        );
1312        assert_eq!(
1313            DateTimeRange::from_start_to_end_with_time_zone(
1314                offset
1315                    .from_local_datetime(&NaiveDateTime::new(
1316                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1317                        NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1318                    ))
1319                    .unwrap(),
1320                offset
1321                    .from_local_datetime(&NaiveDateTime::new(
1322                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1323                        NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1324                    ))
1325                    .unwrap()
1326            )
1327            .unwrap()
1328            .end(),
1329            Some(PreciseDateTime::TimeZone(
1330                offset
1331                    .from_local_datetime(&NaiveDateTime::new(
1332                        NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1333                        NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1334                    ))
1335                    .unwrap()
1336            ))
1337        );
1338        assert!(matches!(
1339            DateTimeRange::from_start_to_end_with_time_zone(
1340                offset
1341                .from_local_datetime(&NaiveDateTime::new(
1342                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1343                    NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1344                ))
1345                .unwrap(),
1346                offset
1347                .from_local_datetime(&NaiveDateTime::new(
1348                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1349                    NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1350                ))
1351                .unwrap()
1352            )
1353           ,
1354            Err(Error::RangeInversion {
1355                start, end ,.. })
1356                if start == "1990-01-01 01:01:01.000005 -01:00" &&
1357                   end == "1990-01-01 01:01:01.000001 -01:00"
1358        ));
1359    }
1360
1361    #[test]
1362    fn test_datetime_range_naive() {
1363        assert_eq!(
1364            DateTimeRange::from_start(NaiveDateTime::new(
1365                NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1366                NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1367            ))
1368            .start(),
1369            Some(PreciseDateTime::Naive(NaiveDateTime::new(
1370                NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1371                NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1372            )))
1373        );
1374        assert_eq!(
1375            DateTimeRange::from_end(NaiveDateTime::new(
1376                NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1377                NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1378            ))
1379            .end(),
1380            Some(PreciseDateTime::Naive(NaiveDateTime::new(
1381                NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1382                NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1383            )))
1384        );
1385        assert_eq!(
1386            DateTimeRange::from_start_to_end(
1387                NaiveDateTime::new(
1388                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1389                    NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1390                ),
1391                NaiveDateTime::new(
1392                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1393                    NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1394                )
1395            )
1396            .unwrap()
1397            .start(),
1398            Some(PreciseDateTime::Naive(NaiveDateTime::new(
1399                NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1400                NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1401            )))
1402        );
1403        assert_eq!(
1404            DateTimeRange::from_start_to_end(
1405                NaiveDateTime::new(
1406                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1407                    NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1408                ),
1409                NaiveDateTime::new(
1410                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1411                    NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1412                )
1413            )
1414            .unwrap()
1415            .end(),
1416            Some(PreciseDateTime::Naive(NaiveDateTime::new(
1417                NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1418                NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1419            )))
1420        );
1421        assert!(matches!(
1422            DateTimeRange::from_start_to_end(
1423                NaiveDateTime::new(
1424                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1425                    NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
1426                ),
1427                NaiveDateTime::new(
1428                    NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1429                    NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
1430                )
1431            )
1432           ,
1433            Err(Error::RangeInversion {
1434                start, end ,.. })
1435                if start == "1990-01-01 01:01:01.000005" &&
1436                   end == "1990-01-01 01:01:01.000001"
1437        ));
1438    }
1439
1440    #[test]
1441    fn test_parse_date_range() {
1442        assert_eq!(
1443            parse_date_range(b"-19900201").ok(),
1444            Some(DateRange {
1445                start: None,
1446                end: Some(NaiveDate::from_ymd_opt(1990, 2, 1).unwrap())
1447            })
1448        );
1449        assert_eq!(
1450            parse_date_range(b"-202002").ok(),
1451            Some(DateRange {
1452                start: None,
1453                end: Some(NaiveDate::from_ymd_opt(2020, 2, 29).unwrap())
1454            })
1455        );
1456        assert_eq!(
1457            parse_date_range(b"-0020").ok(),
1458            Some(DateRange {
1459                start: None,
1460                end: Some(NaiveDate::from_ymd_opt(20, 12, 31).unwrap())
1461            })
1462        );
1463        assert_eq!(
1464            parse_date_range(b"0002-").ok(),
1465            Some(DateRange {
1466                start: Some(NaiveDate::from_ymd_opt(2, 1, 1).unwrap()),
1467                end: None
1468            })
1469        );
1470        assert_eq!(
1471            parse_date_range(b"000203-").ok(),
1472            Some(DateRange {
1473                start: Some(NaiveDate::from_ymd_opt(2, 3, 1).unwrap()),
1474                end: None
1475            })
1476        );
1477        assert_eq!(
1478            parse_date_range(b"00020307-").ok(),
1479            Some(DateRange {
1480                start: Some(NaiveDate::from_ymd_opt(2, 3, 7).unwrap()),
1481                end: None
1482            })
1483        );
1484        assert_eq!(
1485            parse_date_range(b"0002-202002  ").ok(),
1486            Some(DateRange {
1487                start: Some(NaiveDate::from_ymd_opt(2, 1, 1).unwrap()),
1488                end: Some(NaiveDate::from_ymd_opt(2020, 2, 29).unwrap())
1489            })
1490        );
1491        assert!(parse_date_range(b"0002").is_err());
1492        assert!(parse_date_range(b"0002x").is_err());
1493        assert!(parse_date_range(b" 2010-2020").is_err());
1494    }
1495
1496    #[test]
1497    fn test_parse_time_range() {
1498        assert_eq!(
1499            parse_time_range(b"-101010.123456789").ok(),
1500            Some(TimeRange {
1501                start: None,
1502                end: Some(NaiveTime::from_hms_micro_opt(10, 10, 10, 123_456).unwrap())
1503            })
1504        );
1505        assert_eq!(
1506            parse_time_range(b"-101010.123 ").ok(),
1507            Some(TimeRange {
1508                start: None,
1509                end: Some(NaiveTime::from_hms_micro_opt(10, 10, 10, 123_999).unwrap())
1510            })
1511        );
1512        assert_eq!(
1513            parse_time_range(b"-01 ").ok(),
1514            Some(TimeRange {
1515                start: None,
1516                end: Some(NaiveTime::from_hms_micro_opt(1, 59, 59, 999_999).unwrap())
1517            })
1518        );
1519        assert_eq!(
1520            parse_time_range(b"101010.123456-").ok(),
1521            Some(TimeRange {
1522                start: Some(NaiveTime::from_hms_micro_opt(10, 10, 10, 123_456).unwrap()),
1523                end: None
1524            })
1525        );
1526        assert_eq!(
1527            parse_time_range(b"101010.123-").ok(),
1528            Some(TimeRange {
1529                start: Some(NaiveTime::from_hms_micro_opt(10, 10, 10, 123_000).unwrap()),
1530                end: None
1531            })
1532        );
1533        assert_eq!(
1534            parse_time_range(b"1010-").ok(),
1535            Some(TimeRange {
1536                start: Some(NaiveTime::from_hms_opt(10, 10, 0).unwrap()),
1537                end: None
1538            })
1539        );
1540        assert_eq!(
1541            parse_time_range(b"00-").ok(),
1542            Some(TimeRange {
1543                start: Some(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
1544                end: None
1545            })
1546        );
1547    }
1548
1549    #[test]
1550    fn test_parse_datetime_range() {
1551        assert_eq!(
1552            parse_datetime_range(b"-20200229153420.123456").ok(),
1553            Some(DateTimeRange::Naive {
1554                start: None,
1555                end: Some(NaiveDateTime::new(
1556                    NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1557                    NaiveTime::from_hms_micro_opt(15, 34, 20, 123_456).unwrap()
1558                ))
1559            })
1560        );
1561        assert_eq!(
1562            parse_datetime_range(b"-20200229153420.123").ok(),
1563            Some(DateTimeRange::Naive {
1564                start: None,
1565                end: Some(NaiveDateTime::new(
1566                    NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1567                    NaiveTime::from_hms_micro_opt(15, 34, 20, 123_999).unwrap()
1568                ))
1569            })
1570        );
1571        assert_eq!(
1572            parse_datetime_range(b"-20200229153420").ok(),
1573            Some(DateTimeRange::Naive {
1574                start: None,
1575                end: Some(NaiveDateTime::new(
1576                    NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1577                    NaiveTime::from_hms_micro_opt(15, 34, 20, 999_999).unwrap()
1578                ))
1579            })
1580        );
1581        assert_eq!(
1582            parse_datetime_range(b"-2020022915").ok(),
1583            Some(DateTimeRange::Naive {
1584                start: None,
1585                end: Some(NaiveDateTime::new(
1586                    NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1587                    NaiveTime::from_hms_micro_opt(15, 59, 59, 999_999).unwrap()
1588                ))
1589            })
1590        );
1591        assert_eq!(
1592            parse_datetime_range(b"-202002").ok(),
1593            Some(DateTimeRange::Naive {
1594                start: None,
1595                end: Some(NaiveDateTime::new(
1596                    NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1597                    NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1598                ))
1599            })
1600        );
1601        assert_eq!(
1602            parse_datetime_range(b"0002-").ok(),
1603            Some(DateTimeRange::Naive {
1604                start: Some(NaiveDateTime::new(
1605                    NaiveDate::from_ymd_opt(2, 1, 1).unwrap(),
1606                    NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1607                )),
1608                end: None
1609            })
1610        );
1611        assert_eq!(
1612            parse_datetime_range(b"00021231-").ok(),
1613            Some(DateTimeRange::Naive {
1614                start: Some(NaiveDateTime::new(
1615                    NaiveDate::from_ymd_opt(2, 12, 31).unwrap(),
1616                    NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1617                )),
1618                end: None
1619            })
1620        );
1621        // two 'east' UTC offsets get parsed
1622        assert_eq!(
1623            parse_datetime_range(b"19900101+0500-1999+1400").ok(),
1624            Some(DateTimeRange::TimeZone {
1625                start: Some(
1626                    FixedOffset::east_opt(5 * 3600)
1627                        .unwrap()
1628                        .from_local_datetime(&NaiveDateTime::new(
1629                            NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1630                            NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1631                        ))
1632                        .unwrap()
1633                ),
1634                end: Some(
1635                    FixedOffset::east_opt(14 * 3600)
1636                        .unwrap()
1637                        .from_local_datetime(&NaiveDateTime::new(
1638                            NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
1639                            NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1640                        ))
1641                        .unwrap()
1642                )
1643            })
1644        );
1645        // two 'west' Time zone offsets get parsed
1646        assert_eq!(
1647            parse_datetime_range(b"19900101-0500-1999-1200").ok(),
1648            Some(DateTimeRange::TimeZone {
1649                start: Some(
1650                    FixedOffset::west_opt(5 * 3600)
1651                        .unwrap()
1652                        .from_local_datetime(&NaiveDateTime::new(
1653                            NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1654                            NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1655                        ))
1656                        .unwrap()
1657                ),
1658                end: Some(
1659                    FixedOffset::west_opt(12 * 3600)
1660                        .unwrap()
1661                        .from_local_datetime(&NaiveDateTime::new(
1662                            NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
1663                            NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1664                        ))
1665                        .unwrap()
1666                )
1667            })
1668        );
1669        // 'east' and 'west' Time zone offsets get parsed
1670        assert_eq!(
1671            parse_datetime_range(b"19900101+1400-1999-1200").ok(),
1672            Some(DateTimeRange::TimeZone {
1673                start: Some(
1674                    FixedOffset::east_opt(14 * 3600)
1675                        .unwrap()
1676                        .from_local_datetime(&NaiveDateTime::new(
1677                            NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1678                            NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1679                        ))
1680                        .unwrap()
1681                ),
1682                end: Some(
1683                    FixedOffset::west_opt(12 * 3600)
1684                        .unwrap()
1685                        .from_local_datetime(&NaiveDateTime::new(
1686                            NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
1687                            NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1688                        ))
1689                        .unwrap()
1690                )
1691            })
1692        );
1693        // one 'west' Time zone offset gets parsed, offset cannot be mistaken for a date-time
1694        // the missing Time zone offset will be replaced with local clock time-zone offset (default behavior)
1695        assert_eq!(
1696            parse_datetime_range(b"19900101-1200-1999").unwrap(),
1697            DateTimeRange::TimeZone {
1698                start: Some(
1699                    FixedOffset::west_opt(12 * 3600)
1700                        .unwrap()
1701                        .from_local_datetime(&NaiveDateTime::new(
1702                            NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
1703                            NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1704                        ))
1705                        .unwrap()
1706                ),
1707                end: Some(
1708                    Local::now()
1709                        .offset()
1710                        .from_local_datetime(&NaiveDateTime::new(
1711                            NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
1712                            NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1713                        ))
1714                        .unwrap()
1715                )
1716            }
1717        );
1718        // '0500' can either be a valid west UTC offset on the lower bound, or a valid date-time on the upper bound
1719        // Now, the first dash is considered to be a range separator, so the lower bound time-zone offset is missing
1720        // and will be considered to be the local clock time-zone offset.
1721        assert_eq!(
1722            parse_datetime_range(b"0050-0500-1000").unwrap(),
1723            DateTimeRange::TimeZone {
1724                start: Some(
1725                    Local::now()
1726                        .offset()
1727                        .from_local_datetime(&NaiveDateTime::new(
1728                            NaiveDate::from_ymd_opt(50, 1, 1).unwrap(),
1729                            NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1730                        ))
1731                        .unwrap()
1732                ),
1733                end: Some(
1734                    FixedOffset::west_opt(10 * 3600)
1735                        .unwrap()
1736                        .from_local_datetime(&NaiveDateTime::new(
1737                            NaiveDate::from_ymd_opt(500, 12, 31).unwrap(),
1738                            NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1739                        ))
1740                        .unwrap()
1741                )
1742            }
1743        );
1744        // sequence with more than 3 dashes '-' is refused.
1745        assert!(matches!(
1746            parse_datetime_range(b"0001-00021231-2021-0100-0100"),
1747            Err(Error::SeparatorCount { .. })
1748        ));
1749        // any sequence without a dash '-' is refused.
1750        assert!(matches!(
1751            parse_datetime_range(b"00021231+0500"),
1752            Err(Error::NoRangeSeparator { .. })
1753        ));
1754    }
1755}