Skip to main content

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