use chrono::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
use snafu::{Backtrace, OptionExt, ResultExt, Snafu};
use crate::value::deserialize::{
parse_date_partial, parse_datetime_partial, parse_time_partial, Error as DeserializeError,
};
use crate::value::partial::{DicomDate, DicomDateTime, DicomTime, PreciseDateTime};
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum Error {
#[snafu(display("Unexpected end of element"))]
UnexpectedEndOfElement { backtrace: Backtrace },
#[snafu(display("Failed to parse value"))]
Parse {
#[snafu(backtrace)]
source: DeserializeError,
},
#[snafu(display("End {} is before start {}", end, start))]
RangeInversion {
start: String,
end: String,
backtrace: Backtrace,
},
#[snafu(display("No range separator present"))]
NoRangeSeparator { backtrace: Backtrace },
#[snafu(display("Date-time range can contain 1-3 '-' characters, {} were found", value))]
SeparatorCount { value: usize, backtrace: Backtrace },
#[snafu(display("Converting a time-zone naive value '{naive}' to a time-zone '{offset}' leads to invalid date-time or ambiguous results."))]
InvalidDateTime {
naive: NaiveDateTime,
offset: FixedOffset,
backtrace: Backtrace,
},
#[snafu(display(
"Cannot convert from an imprecise value. This value represents a date / time range"
))]
ImpreciseValue { backtrace: Backtrace },
#[snafu(display("Failed to construct Date from '{y}-{m}-{d}'"))]
InvalidDate {
y: i32,
m: u32,
d: u32,
backtrace: Backtrace,
},
#[snafu(display("Failed to construct Time from {h}:{m}:{s}"))]
InvalidTime {
h: u32,
m: u32,
s: u32,
backtrace: Backtrace,
},
#[snafu(display("Failed to construct Time from {h}:{m}:{s}:{f}"))]
InvalidTimeMicro {
h: u32,
m: u32,
s: u32,
f: u32,
backtrace: Backtrace,
},
#[snafu(display("Use 'to_precise_datetime' to retrieve a precise value from a date-time"))]
ToPreciseDateTime { backtrace: Backtrace },
#[snafu(display(
"Parsing a date-time range from '{start}' to '{end}' with only one time-zone '{time_zone} value, second time-zone is missing.'"
))]
AmbiguousDtRange {
start: NaiveDateTime,
end: NaiveDateTime,
time_zone: FixedOffset,
backtrace: Backtrace,
},
}
type Result<T, E = Error> = std::result::Result<T, E>;
pub trait AsRange {
type PreciseValue: PartialEq + PartialOrd;
type Range;
fn is_precise(&self) -> bool;
fn exact(&self) -> Result<Self::PreciseValue> {
if self.is_precise() {
Ok(self.earliest()?)
} else {
ImpreciseValueSnafu.fail()
}
}
fn earliest(&self) -> Result<Self::PreciseValue>;
fn latest(&self) -> Result<Self::PreciseValue>;
fn range(&self) -> Result<Self::Range>;
}
impl AsRange for DicomDate {
type PreciseValue = NaiveDate;
type Range = DateRange;
fn is_precise(&self) -> bool {
self.day().is_some()
}
fn earliest(&self) -> Result<Self::PreciseValue> {
let (y, m, d) = {
(
*self.year() as i32,
*self.month().unwrap_or(&1) as u32,
*self.day().unwrap_or(&1) as u32,
)
};
NaiveDate::from_ymd_opt(y, m, d).context(InvalidDateSnafu { y, m, d })
}
fn latest(&self) -> Result<Self::PreciseValue> {
let (y, m, d) = (
self.year(),
self.month().unwrap_or(&12),
match self.day() {
Some(d) => *d as u32,
None => {
let y = self.year();
let m = self.month().unwrap_or(&12);
if m == &12 {
NaiveDate::from_ymd_opt(*y as i32 + 1, 1, 1).context(InvalidDateSnafu {
y: *y as i32,
m: 1u32,
d: 1u32,
})?
} else {
NaiveDate::from_ymd_opt(*y as i32, *m as u32 + 1, 1).context(
InvalidDateSnafu {
y: *y as i32,
m: *m as u32,
d: 1u32,
},
)?
}
.signed_duration_since(
NaiveDate::from_ymd_opt(*y as i32, *m as u32, 1).context(
InvalidDateSnafu {
y: *y as i32,
m: *m as u32,
d: 1u32,
},
)?,
)
.num_days() as u32
}
},
);
NaiveDate::from_ymd_opt(*y as i32, *m as u32, d).context(InvalidDateSnafu {
y: *y as i32,
m: *m as u32,
d,
})
}
fn range(&self) -> Result<Self::Range> {
let start = self.earliest()?;
let end = self.latest()?;
DateRange::from_start_to_end(start, end)
}
}
impl AsRange for DicomTime {
type PreciseValue = NaiveTime;
type Range = TimeRange;
fn is_precise(&self) -> bool {
matches!(self.fraction_and_precision(), Some((_fr_, precision)) if precision == 6)
}
fn earliest(&self) -> Result<Self::PreciseValue> {
let (h, m, s, f) = (
self.hour(),
self.minute().unwrap_or(&0),
self.second().unwrap_or(&0),
match self.fraction_and_precision() {
None => 0,
Some((f, fp)) => f * u32::pow(10, 6 - <u32>::from(fp)),
},
);
NaiveTime::from_hms_micro_opt((*h).into(), (*m).into(), (*s).into(), f).context(
InvalidTimeMicroSnafu {
h: *h as u32,
m: *m as u32,
s: *s as u32,
f,
},
)
}
fn latest(&self) -> Result<Self::PreciseValue> {
let (h, m, s, f) = (
self.hour(),
self.minute().unwrap_or(&59),
self.second().unwrap_or(&59),
match self.fraction_and_precision() {
None => 999_999,
Some((f, fp)) => {
(f * u32::pow(10, 6 - u32::from(fp))) + (u32::pow(10, 6 - u32::from(fp))) - 1
}
},
);
NaiveTime::from_hms_micro_opt((*h).into(), (*m).into(), (*s).into(), f).context(
InvalidTimeMicroSnafu {
h: *h as u32,
m: *m as u32,
s: *s as u32,
f,
},
)
}
fn range(&self) -> Result<Self::Range> {
let start = self.earliest()?;
let end = self.latest()?;
TimeRange::from_start_to_end(start, end)
}
}
impl AsRange for DicomDateTime {
type PreciseValue = PreciseDateTime;
type Range = DateTimeRange;
fn is_precise(&self) -> bool {
match self.time() {
Some(dicom_time) => dicom_time.is_precise(),
None => false,
}
}
fn earliest(&self) -> Result<Self::PreciseValue> {
let date = self.date().earliest()?;
let time = match self.time() {
Some(time) => time.earliest()?,
None => NaiveTime::from_hms_opt(0, 0, 0).context(InvalidTimeSnafu {
h: 0u32,
m: 0u32,
s: 0u32,
})?,
};
match self.time_zone() {
Some(offset) => Ok(PreciseDateTime::TimeZone(
offset
.from_local_datetime(&NaiveDateTime::new(date, time))
.single()
.context(InvalidDateTimeSnafu {
naive: NaiveDateTime::new(date, time),
offset: *offset,
})?,
)),
None => Ok(PreciseDateTime::Naive(NaiveDateTime::new(date, time))),
}
}
fn latest(&self) -> Result<Self::PreciseValue> {
let date = self.date().latest()?;
let time = match self.time() {
Some(time) => time.latest()?,
None => NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).context(
InvalidTimeMicroSnafu {
h: 23u32,
m: 59u32,
s: 59u32,
f: 999_999u32,
},
)?,
};
match self.time_zone() {
Some(offset) => Ok(PreciseDateTime::TimeZone(
offset
.from_local_datetime(&NaiveDateTime::new(date, time))
.single()
.context(InvalidDateTimeSnafu {
naive: NaiveDateTime::new(date, time),
offset: *offset,
})?,
)),
None => Ok(PreciseDateTime::Naive(NaiveDateTime::new(date, time))),
}
}
fn range(&self) -> Result<Self::Range> {
let start = self.earliest()?;
let end = self.latest()?;
match (start, end) {
(PreciseDateTime::Naive(start), PreciseDateTime::Naive(end)) => {
DateTimeRange::from_start_to_end(start, end)
}
(PreciseDateTime::TimeZone(start), PreciseDateTime::TimeZone(end)) => {
DateTimeRange::from_start_to_end_with_time_zone(start, end)
}
_ => unreachable!(),
}
}
}
impl DicomDate {
pub fn to_naive_date(self) -> Result<NaiveDate> {
self.exact()
}
}
impl DicomTime {
pub fn to_naive_time(self) -> Result<NaiveTime> {
if self.second().is_some() {
self.earliest()
} else {
ImpreciseValueSnafu.fail()
}
}
}
impl DicomDateTime {
pub fn to_precise_datetime(&self) -> Result<PreciseDateTime> {
self.exact()
}
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub struct DateRange {
start: Option<NaiveDate>,
end: Option<NaiveDate>,
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub struct TimeRange {
start: Option<NaiveTime>,
end: Option<NaiveTime>,
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum DateTimeRange {
Naive {
start: Option<NaiveDateTime>,
end: Option<NaiveDateTime>,
},
TimeZone {
start: Option<DateTime<FixedOffset>>,
end: Option<DateTime<FixedOffset>>,
},
}
impl DateRange {
pub fn from_start_to_end(start: NaiveDate, end: NaiveDate) -> Result<DateRange> {
if start > end {
RangeInversionSnafu {
start: start.to_string(),
end: end.to_string(),
}
.fail()
} else {
Ok(DateRange {
start: Some(start),
end: Some(end),
})
}
}
pub fn from_start(start: NaiveDate) -> DateRange {
DateRange {
start: Some(start),
end: None,
}
}
pub fn from_end(end: NaiveDate) -> DateRange {
DateRange {
start: None,
end: Some(end),
}
}
pub fn start(&self) -> Option<&NaiveDate> {
self.start.as_ref()
}
pub fn end(&self) -> Option<&NaiveDate> {
self.end.as_ref()
}
}
impl TimeRange {
pub fn from_start_to_end(start: NaiveTime, end: NaiveTime) -> Result<TimeRange> {
if start > end {
RangeInversionSnafu {
start: start.to_string(),
end: end.to_string(),
}
.fail()
} else {
Ok(TimeRange {
start: Some(start),
end: Some(end),
})
}
}
pub fn from_start(start: NaiveTime) -> TimeRange {
TimeRange {
start: Some(start),
end: None,
}
}
pub fn from_end(end: NaiveTime) -> TimeRange {
TimeRange {
start: None,
end: Some(end),
}
}
pub fn start(&self) -> Option<&NaiveTime> {
self.start.as_ref()
}
pub fn end(&self) -> Option<&NaiveTime> {
self.end.as_ref()
}
}
impl DateTimeRange {
pub fn from_start_to_end_with_time_zone(
start: DateTime<FixedOffset>,
end: DateTime<FixedOffset>,
) -> Result<DateTimeRange> {
if start > end {
RangeInversionSnafu {
start: start.to_string(),
end: end.to_string(),
}
.fail()
} else {
Ok(DateTimeRange::TimeZone {
start: Some(start),
end: Some(end),
})
}
}
pub fn from_start_to_end(start: NaiveDateTime, end: NaiveDateTime) -> Result<DateTimeRange> {
if start > end {
RangeInversionSnafu {
start: start.to_string(),
end: end.to_string(),
}
.fail()
} else {
Ok(DateTimeRange::Naive {
start: Some(start),
end: Some(end),
})
}
}
pub fn from_start_with_time_zone(start: DateTime<FixedOffset>) -> DateTimeRange {
DateTimeRange::TimeZone {
start: Some(start),
end: None,
}
}
pub fn from_start(start: NaiveDateTime) -> DateTimeRange {
DateTimeRange::Naive {
start: Some(start),
end: None,
}
}
pub fn from_end_with_time_zone(end: DateTime<FixedOffset>) -> DateTimeRange {
DateTimeRange::TimeZone {
start: None,
end: Some(end),
}
}
pub fn from_end(end: NaiveDateTime) -> DateTimeRange {
DateTimeRange::Naive {
start: None,
end: Some(end),
}
}
pub fn start(&self) -> Option<PreciseDateTime> {
match self {
DateTimeRange::Naive { start, .. } => start.map(PreciseDateTime::Naive),
DateTimeRange::TimeZone { start, .. } => start.map(PreciseDateTime::TimeZone),
}
}
pub fn end(&self) -> Option<PreciseDateTime> {
match self {
DateTimeRange::Naive { start: _, end } => end.map(PreciseDateTime::Naive),
DateTimeRange::TimeZone { start: _, end } => end.map(PreciseDateTime::TimeZone),
}
}
pub fn from_date_and_time_range(dr: DateRange, tr: TimeRange) -> Result<DateTimeRange> {
let start_date = dr.start();
let end_date = dr.end();
let start_time = *tr
.start()
.unwrap_or(&NaiveTime::from_hms_opt(0, 0, 0).context(InvalidTimeSnafu {
h: 0u32,
m: 0u32,
s: 0u32,
})?);
let end_time =
*tr.end()
.unwrap_or(&NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).context(
InvalidTimeMicroSnafu {
h: 23u32,
m: 59u32,
s: 59u32,
f: 999_999u32,
},
)?);
match start_date {
Some(sd) => match end_date {
Some(ed) => Ok(DateTimeRange::from_start_to_end(
NaiveDateTime::new(*sd, start_time),
NaiveDateTime::new(*ed, end_time),
)?),
None => Ok(DateTimeRange::from_start(NaiveDateTime::new(
*sd, start_time,
))),
},
None => match end_date {
Some(ed) => Ok(DateTimeRange::from_end(NaiveDateTime::new(*ed, end_time))),
None => panic!("Impossible combination of two None values for a date range."),
},
}
}
}
pub fn parse_date_range(buf: &[u8]) -> Result<DateRange> {
if buf.len() < 5 {
return UnexpectedEndOfElementSnafu.fail();
}
if let Some(separator) = buf.iter().position(|e| *e == b'-') {
let (start, end) = buf.split_at(separator);
let end = &end[1..];
match separator {
0 => Ok(DateRange::from_end(
parse_date_partial(end).context(ParseSnafu)?.0.latest()?,
)),
i if i == buf.len() - 1 => Ok(DateRange::from_start(
parse_date_partial(start)
.context(ParseSnafu)?
.0
.earliest()?,
)),
_ => Ok(DateRange::from_start_to_end(
parse_date_partial(start)
.context(ParseSnafu)?
.0
.earliest()?,
parse_date_partial(end).context(ParseSnafu)?.0.latest()?,
)?),
}
} else {
NoRangeSeparatorSnafu.fail()
}
}
pub fn parse_time_range(buf: &[u8]) -> Result<TimeRange> {
if buf.len() < 3 {
return UnexpectedEndOfElementSnafu.fail();
}
if let Some(separator) = buf.iter().position(|e| *e == b'-') {
let (start, end) = buf.split_at(separator);
let end = &end[1..];
match separator {
0 => Ok(TimeRange::from_end(
parse_time_partial(end).context(ParseSnafu)?.0.latest()?,
)),
i if i == buf.len() - 1 => Ok(TimeRange::from_start(
parse_time_partial(start)
.context(ParseSnafu)?
.0
.earliest()?,
)),
_ => Ok(TimeRange::from_start_to_end(
parse_time_partial(start)
.context(ParseSnafu)?
.0
.earliest()?,
parse_time_partial(end).context(ParseSnafu)?.0.latest()?,
)?),
}
} else {
NoRangeSeparatorSnafu.fail()
}
}
pub trait AmbiguousDtRangeParser {
fn parse_with_ambiguous_start(
ambiguous_start: NaiveDateTime,
end: DateTime<FixedOffset>,
) -> Result<DateTimeRange>;
fn parse_with_ambiguous_end(
start: DateTime<FixedOffset>,
ambiguous_end: NaiveDateTime,
) -> Result<DateTimeRange>;
}
#[derive(Debug)]
pub struct ToLocalTimeZone;
#[derive(Debug)]
pub struct ToKnownTimeZone;
#[derive(Debug)]
pub struct FailOnAmbiguousRange;
#[derive(Debug)]
pub struct IgnoreTimeZone;
impl AmbiguousDtRangeParser for ToKnownTimeZone {
fn parse_with_ambiguous_start(
ambiguous_start: NaiveDateTime,
end: DateTime<FixedOffset>,
) -> Result<DateTimeRange> {
let start = end
.offset()
.from_local_datetime(&ambiguous_start)
.single()
.context(InvalidDateTimeSnafu {
naive: ambiguous_start,
offset: *end.offset(),
})?;
if start > end {
RangeInversionSnafu {
start: ambiguous_start.to_string(),
end: end.to_string(),
}
.fail()
} else {
Ok(DateTimeRange::TimeZone {
start: Some(start),
end: Some(end),
})
}
}
fn parse_with_ambiguous_end(
start: DateTime<FixedOffset>,
ambiguous_end: NaiveDateTime,
) -> Result<DateTimeRange> {
let end = start
.offset()
.from_local_datetime(&ambiguous_end)
.single()
.context(InvalidDateTimeSnafu {
naive: ambiguous_end,
offset: *start.offset(),
})?;
if start > end {
RangeInversionSnafu {
start: start.to_string(),
end: ambiguous_end.to_string(),
}
.fail()
} else {
Ok(DateTimeRange::TimeZone {
start: Some(start),
end: Some(end),
})
}
}
}
impl AmbiguousDtRangeParser for FailOnAmbiguousRange {
fn parse_with_ambiguous_end(
start: DateTime<FixedOffset>,
end: NaiveDateTime,
) -> Result<DateTimeRange> {
let time_zone = *start.offset();
let start = start.naive_local();
AmbiguousDtRangeSnafu {
start,
end,
time_zone,
}
.fail()
}
fn parse_with_ambiguous_start(
start: NaiveDateTime,
end: DateTime<FixedOffset>,
) -> Result<DateTimeRange> {
let time_zone = *end.offset();
let end = end.naive_local();
AmbiguousDtRangeSnafu {
start,
end,
time_zone,
}
.fail()
}
}
impl AmbiguousDtRangeParser for ToLocalTimeZone {
fn parse_with_ambiguous_start(
ambiguous_start: NaiveDateTime,
end: DateTime<FixedOffset>,
) -> Result<DateTimeRange> {
let start = Local::now()
.offset()
.from_local_datetime(&ambiguous_start)
.single()
.context(InvalidDateTimeSnafu {
naive: ambiguous_start,
offset: *end.offset(),
})?;
if start > end {
RangeInversionSnafu {
start: ambiguous_start.to_string(),
end: end.to_string(),
}
.fail()
} else {
Ok(DateTimeRange::TimeZone {
start: Some(start),
end: Some(end),
})
}
}
fn parse_with_ambiguous_end(
start: DateTime<FixedOffset>,
ambiguous_end: NaiveDateTime,
) -> Result<DateTimeRange> {
let end = Local::now()
.offset()
.from_local_datetime(&ambiguous_end)
.single()
.context(InvalidDateTimeSnafu {
naive: ambiguous_end,
offset: *start.offset(),
})?;
if start > end {
RangeInversionSnafu {
start: start.to_string(),
end: ambiguous_end.to_string(),
}
.fail()
} else {
Ok(DateTimeRange::TimeZone {
start: Some(start),
end: Some(end),
})
}
}
}
impl AmbiguousDtRangeParser for IgnoreTimeZone {
fn parse_with_ambiguous_start(
ambiguous_start: NaiveDateTime,
end: DateTime<FixedOffset>,
) -> Result<DateTimeRange> {
let end = end.naive_local();
if ambiguous_start > end {
RangeInversionSnafu {
start: ambiguous_start.to_string(),
end: end.to_string(),
}
.fail()
} else {
Ok(DateTimeRange::Naive {
start: Some(ambiguous_start),
end: Some(end),
})
}
}
fn parse_with_ambiguous_end(
start: DateTime<FixedOffset>,
ambiguous_end: NaiveDateTime,
) -> Result<DateTimeRange> {
let start = start.naive_local();
if start > ambiguous_end {
RangeInversionSnafu {
start: start.to_string(),
end: ambiguous_end.to_string(),
}
.fail()
} else {
Ok(DateTimeRange::Naive {
start: Some(start),
end: Some(ambiguous_end),
})
}
}
}
pub fn parse_datetime_range(buf: &[u8]) -> Result<DateTimeRange> {
parse_datetime_range_impl::<ToLocalTimeZone>(buf)
}
pub fn parse_datetime_range_custom<T: AmbiguousDtRangeParser>(buf: &[u8]) -> Result<DateTimeRange> {
parse_datetime_range_impl::<T>(buf)
}
pub fn parse_datetime_range_impl<T: AmbiguousDtRangeParser>(buf: &[u8]) -> Result<DateTimeRange> {
if buf.len() < 5 {
return UnexpectedEndOfElementSnafu.fail();
}
if buf[0] == b'-' {
let buf = &buf[1..];
match parse_datetime_partial(buf).context(ParseSnafu)?.latest()? {
PreciseDateTime::Naive(end) => Ok(DateTimeRange::from_end(end)),
PreciseDateTime::TimeZone(end_tz) => Ok(DateTimeRange::from_end_with_time_zone(end_tz)),
}
} else if buf[buf.len() - 1] == b'-' {
let buf = &buf[0..(buf.len() - 1)];
match parse_datetime_partial(buf)
.context(ParseSnafu)?
.earliest()?
{
PreciseDateTime::Naive(start) => Ok(DateTimeRange::from_start(start)),
PreciseDateTime::TimeZone(start_tz) => {
Ok(DateTimeRange::from_start_with_time_zone(start_tz))
}
}
} else {
let dashes: Vec<usize> = buf
.iter()
.enumerate()
.filter(|(_i, c)| **c == b'-')
.map(|(i, _c)| i)
.collect();
let separator = match dashes.len() {
0 => return NoRangeSeparatorSnafu.fail(), 1 => dashes[0], 2 => {
let (start1, end1) = buf.split_at(dashes[0]);
let first = (
parse_datetime_partial(start1),
parse_datetime_partial(&end1[1..]),
);
match first {
(Ok(s), Ok(e)) => {
let dtr = match (s.earliest()?, e.latest()?) {
(PreciseDateTime::Naive(start), PreciseDateTime::Naive(end)) => {
DateTimeRange::from_start_to_end(start, end)
}
(PreciseDateTime::TimeZone(start), PreciseDateTime::TimeZone(end)) => {
DateTimeRange::from_start_to_end_with_time_zone(start, end)
}
(
PreciseDateTime::Naive(start),
PreciseDateTime::TimeZone(end),
) => T::parse_with_ambiguous_start(start, end),
(
PreciseDateTime::TimeZone(start),
PreciseDateTime::Naive(end),
) => T::parse_with_ambiguous_end(start, end),
};
match dtr {
Ok(val) => return Ok(val),
Err(_) => dashes[1],
}
}
_ => dashes[1],
}
}
3 => dashes[1], len => return SeparatorCountSnafu { value: len }.fail(),
};
let (start, end) = buf.split_at(separator);
let end = &end[1..];
match (
parse_datetime_partial(start)
.context(ParseSnafu)?
.earliest()?,
parse_datetime_partial(end).context(ParseSnafu)?.latest()?,
) {
(PreciseDateTime::Naive(start), PreciseDateTime::Naive(end)) => {
DateTimeRange::from_start_to_end(start, end)
}
(PreciseDateTime::TimeZone(start), PreciseDateTime::TimeZone(end)) => {
DateTimeRange::from_start_to_end_with_time_zone(start, end)
}
(PreciseDateTime::Naive(start), PreciseDateTime::TimeZone(end)) => {
T::parse_with_ambiguous_start(start, end)
}
(PreciseDateTime::TimeZone(start), PreciseDateTime::Naive(end)) => {
T::parse_with_ambiguous_end(start, end)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_date_range() {
assert_eq!(
DateRange::from_start(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()).start(),
Some(&NaiveDate::from_ymd_opt(2020, 1, 1).unwrap())
);
assert_eq!(
DateRange::from_end(NaiveDate::from_ymd_opt(2020, 12, 31).unwrap()).end(),
Some(&NaiveDate::from_ymd_opt(2020, 12, 31).unwrap())
);
assert_eq!(
DateRange::from_start_to_end(
NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
NaiveDate::from_ymd_opt(2020, 12, 31).unwrap()
)
.unwrap()
.start(),
Some(&NaiveDate::from_ymd_opt(2020, 1, 1).unwrap())
);
assert_eq!(
DateRange::from_start_to_end(
NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
NaiveDate::from_ymd_opt(2020, 12, 31).unwrap()
)
.unwrap()
.end(),
Some(&NaiveDate::from_ymd_opt(2020, 12, 31).unwrap())
);
assert!(matches!(
DateRange::from_start_to_end(
NaiveDate::from_ymd_opt(2020, 12, 1).unwrap(),
NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()
),
Err(Error::RangeInversion {
start, end ,.. }) if start == "2020-12-01" && end == "2020-01-01"
));
}
#[test]
fn test_time_range() {
assert_eq!(
TimeRange::from_start(NaiveTime::from_hms_opt(5, 5, 5).unwrap()).start(),
Some(&NaiveTime::from_hms_opt(5, 5, 5).unwrap())
);
assert_eq!(
TimeRange::from_end(NaiveTime::from_hms_opt(5, 5, 5).unwrap()).end(),
Some(&NaiveTime::from_hms_opt(5, 5, 5).unwrap())
);
assert_eq!(
TimeRange::from_start_to_end(
NaiveTime::from_hms_opt(5, 5, 5).unwrap(),
NaiveTime::from_hms_opt(5, 5, 6).unwrap()
)
.unwrap()
.start(),
Some(&NaiveTime::from_hms_opt(5, 5, 5).unwrap())
);
assert_eq!(
TimeRange::from_start_to_end(
NaiveTime::from_hms_opt(5, 5, 5).unwrap(),
NaiveTime::from_hms_opt(5, 5, 6).unwrap()
)
.unwrap()
.end(),
Some(&NaiveTime::from_hms_opt(5, 5, 6).unwrap())
);
assert!(matches!(
TimeRange::from_start_to_end(
NaiveTime::from_hms_micro_opt(5, 5, 5, 123_456).unwrap(),
NaiveTime::from_hms_micro_opt(5, 5, 5, 123_450).unwrap()
),
Err(Error::RangeInversion {
start, end ,.. }) if start == "05:05:05.123456" && end == "05:05:05.123450"
));
}
#[test]
fn test_datetime_range_with_time_zone() {
let offset = FixedOffset::west_opt(3600).unwrap();
assert_eq!(
DateTimeRange::from_start_with_time_zone(
offset
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
))
.unwrap()
)
.start(),
Some(PreciseDateTime::TimeZone(
offset
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
))
.unwrap()
))
);
assert_eq!(
DateTimeRange::from_end_with_time_zone(
offset
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
))
.unwrap()
)
.end(),
Some(PreciseDateTime::TimeZone(
offset
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
))
.unwrap()
))
);
assert_eq!(
DateTimeRange::from_start_to_end_with_time_zone(
offset
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
))
.unwrap(),
offset
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
))
.unwrap()
)
.unwrap()
.start(),
Some(PreciseDateTime::TimeZone(
offset
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
))
.unwrap()
))
);
assert_eq!(
DateTimeRange::from_start_to_end_with_time_zone(
offset
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
))
.unwrap(),
offset
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
))
.unwrap()
)
.unwrap()
.end(),
Some(PreciseDateTime::TimeZone(
offset
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
))
.unwrap()
))
);
assert!(matches!(
DateTimeRange::from_start_to_end_with_time_zone(
offset
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
))
.unwrap(),
offset
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
))
.unwrap()
)
,
Err(Error::RangeInversion {
start, end ,.. })
if start == "1990-01-01 01:01:01.000005 -01:00" &&
end == "1990-01-01 01:01:01.000001 -01:00"
));
}
#[test]
fn test_datetime_range_naive() {
assert_eq!(
DateTimeRange::from_start(NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
))
.start(),
Some(PreciseDateTime::Naive(NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
)))
);
assert_eq!(
DateTimeRange::from_end(NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
))
.end(),
Some(PreciseDateTime::Naive(NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
)))
);
assert_eq!(
DateTimeRange::from_start_to_end(
NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
),
NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
)
)
.unwrap()
.start(),
Some(PreciseDateTime::Naive(NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
)))
);
assert_eq!(
DateTimeRange::from_start_to_end(
NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
),
NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
)
)
.unwrap()
.end(),
Some(PreciseDateTime::Naive(NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
)))
);
assert!(matches!(
DateTimeRange::from_start_to_end(
NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap()
),
NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap()
)
)
,
Err(Error::RangeInversion {
start, end ,.. })
if start == "1990-01-01 01:01:01.000005" &&
end == "1990-01-01 01:01:01.000001"
));
}
#[test]
fn test_parse_date_range() {
assert_eq!(
parse_date_range(b"-19900201").ok(),
Some(DateRange {
start: None,
end: Some(NaiveDate::from_ymd_opt(1990, 2, 1).unwrap())
})
);
assert_eq!(
parse_date_range(b"-202002").ok(),
Some(DateRange {
start: None,
end: Some(NaiveDate::from_ymd_opt(2020, 2, 29).unwrap())
})
);
assert_eq!(
parse_date_range(b"-0020").ok(),
Some(DateRange {
start: None,
end: Some(NaiveDate::from_ymd_opt(20, 12, 31).unwrap())
})
);
assert_eq!(
parse_date_range(b"0002-").ok(),
Some(DateRange {
start: Some(NaiveDate::from_ymd_opt(2, 1, 1).unwrap()),
end: None
})
);
assert_eq!(
parse_date_range(b"000203-").ok(),
Some(DateRange {
start: Some(NaiveDate::from_ymd_opt(2, 3, 1).unwrap()),
end: None
})
);
assert_eq!(
parse_date_range(b"00020307-").ok(),
Some(DateRange {
start: Some(NaiveDate::from_ymd_opt(2, 3, 7).unwrap()),
end: None
})
);
assert_eq!(
parse_date_range(b"0002-202002 ").ok(),
Some(DateRange {
start: Some(NaiveDate::from_ymd_opt(2, 1, 1).unwrap()),
end: Some(NaiveDate::from_ymd_opt(2020, 2, 29).unwrap())
})
);
assert!(parse_date_range(b"0002").is_err());
assert!(parse_date_range(b"0002x").is_err());
assert!(parse_date_range(b" 2010-2020").is_err());
}
#[test]
fn test_parse_time_range() {
assert_eq!(
parse_time_range(b"-101010.123456789").ok(),
Some(TimeRange {
start: None,
end: Some(NaiveTime::from_hms_micro_opt(10, 10, 10, 123_456).unwrap())
})
);
assert_eq!(
parse_time_range(b"-101010.123 ").ok(),
Some(TimeRange {
start: None,
end: Some(NaiveTime::from_hms_micro_opt(10, 10, 10, 123_999).unwrap())
})
);
assert_eq!(
parse_time_range(b"-01 ").ok(),
Some(TimeRange {
start: None,
end: Some(NaiveTime::from_hms_micro_opt(1, 59, 59, 999_999).unwrap())
})
);
assert_eq!(
parse_time_range(b"101010.123456-").ok(),
Some(TimeRange {
start: Some(NaiveTime::from_hms_micro_opt(10, 10, 10, 123_456).unwrap()),
end: None
})
);
assert_eq!(
parse_time_range(b"101010.123-").ok(),
Some(TimeRange {
start: Some(NaiveTime::from_hms_micro_opt(10, 10, 10, 123_000).unwrap()),
end: None
})
);
assert_eq!(
parse_time_range(b"1010-").ok(),
Some(TimeRange {
start: Some(NaiveTime::from_hms_opt(10, 10, 0).unwrap()),
end: None
})
);
assert_eq!(
parse_time_range(b"00-").ok(),
Some(TimeRange {
start: Some(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
end: None
})
);
}
#[test]
fn test_parse_datetime_range() {
assert_eq!(
parse_datetime_range(b"-20200229153420.123456").ok(),
Some(DateTimeRange::Naive {
start: None,
end: Some(NaiveDateTime::new(
NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
NaiveTime::from_hms_micro_opt(15, 34, 20, 123_456).unwrap()
))
})
);
assert_eq!(
parse_datetime_range(b"-20200229153420.123").ok(),
Some(DateTimeRange::Naive {
start: None,
end: Some(NaiveDateTime::new(
NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
NaiveTime::from_hms_micro_opt(15, 34, 20, 123_999).unwrap()
))
})
);
assert_eq!(
parse_datetime_range(b"-20200229153420").ok(),
Some(DateTimeRange::Naive {
start: None,
end: Some(NaiveDateTime::new(
NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
NaiveTime::from_hms_micro_opt(15, 34, 20, 999_999).unwrap()
))
})
);
assert_eq!(
parse_datetime_range(b"-2020022915").ok(),
Some(DateTimeRange::Naive {
start: None,
end: Some(NaiveDateTime::new(
NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
NaiveTime::from_hms_micro_opt(15, 59, 59, 999_999).unwrap()
))
})
);
assert_eq!(
parse_datetime_range(b"-202002").ok(),
Some(DateTimeRange::Naive {
start: None,
end: Some(NaiveDateTime::new(
NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
))
})
);
assert_eq!(
parse_datetime_range(b"0002-").ok(),
Some(DateTimeRange::Naive {
start: Some(NaiveDateTime::new(
NaiveDate::from_ymd_opt(2, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
)),
end: None
})
);
assert_eq!(
parse_datetime_range(b"00021231-").ok(),
Some(DateTimeRange::Naive {
start: Some(NaiveDateTime::new(
NaiveDate::from_ymd_opt(2, 12, 31).unwrap(),
NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
)),
end: None
})
);
assert_eq!(
parse_datetime_range(b"19900101+0500-1999+1400").ok(),
Some(DateTimeRange::TimeZone {
start: Some(
FixedOffset::east_opt(5 * 3600)
.unwrap()
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
))
.unwrap()
),
end: Some(
FixedOffset::east_opt(14 * 3600)
.unwrap()
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
))
.unwrap()
)
})
);
assert_eq!(
parse_datetime_range(b"19900101-0500-1999-1200").ok(),
Some(DateTimeRange::TimeZone {
start: Some(
FixedOffset::west_opt(5 * 3600)
.unwrap()
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
))
.unwrap()
),
end: Some(
FixedOffset::west_opt(12 * 3600)
.unwrap()
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
))
.unwrap()
)
})
);
assert_eq!(
parse_datetime_range(b"19900101+1400-1999-1200").ok(),
Some(DateTimeRange::TimeZone {
start: Some(
FixedOffset::east_opt(14 * 3600)
.unwrap()
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
))
.unwrap()
),
end: Some(
FixedOffset::west_opt(12 * 3600)
.unwrap()
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
))
.unwrap()
)
})
);
assert_eq!(
parse_datetime_range(b"19900101-1200-1999").unwrap(),
DateTimeRange::TimeZone {
start: Some(
FixedOffset::west_opt(12 * 3600)
.unwrap()
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
))
.unwrap()
),
end: Some(
Local::now()
.offset()
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(),
NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
))
.unwrap()
)
}
);
assert_eq!(
parse_datetime_range(b"0050-0500-1000").unwrap(),
DateTimeRange::TimeZone {
start: Some(
Local::now()
.offset()
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(50, 1, 1).unwrap(),
NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
))
.unwrap()
),
end: Some(
FixedOffset::west_opt(10 * 3600)
.unwrap()
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(500, 12, 31).unwrap(),
NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
))
.unwrap()
)
}
);
assert!(matches!(
parse_datetime_range(b"0001-00021231-2021-0100-0100"),
Err(Error::SeparatorCount { .. })
));
assert!(matches!(
parse_datetime_range(b"00021231+0500"),
Err(Error::NoRangeSeparator { .. })
));
}
}