From 635b3be2f6b61add1c737626530d781ef5a918d5 Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Sun, 28 Jan 2024 14:25:44 +0100 Subject: [PATCH 01/23] - 1st commit --- core/src/value/partial.rs | 159 +++++++++++++++++++++++++++++--------- core/src/value/range.rs | 64 ++++++++++++++- 2 files changed, 184 insertions(+), 39 deletions(-) diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 6e60a90e8..2727deb6d 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -1,7 +1,7 @@ //! Handling of partial precision of Date, Time and DateTime values. use crate::value::range::AsRange; -use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, NaiveTime, Timelike}; +use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; use snafu::{Backtrace, ResultExt, Snafu}; use std::convert::{TryFrom, TryInto}; use std::fmt; @@ -228,7 +228,7 @@ enum DicomTimeImpl { pub struct DicomDateTime { date: DicomDate, time: Option, - offset: FixedOffset, + offset: Option, } /** @@ -578,11 +578,41 @@ impl DicomDateTime { /** * Constructs a new `DicomDateTime` from a `DicomDate` and a given `FixedOffset`. */ - pub fn from_date(date: DicomDate, offset: FixedOffset) -> DicomDateTime { + pub fn from_date_tz(date: DicomDate, offset: FixedOffset) -> DicomDateTime { DicomDateTime { date, time: None, - offset, + offset: Some(offset), + } + } + + /** + * Constructs a new `DicomDateTime` from a `DicomDate` . + */ + pub fn from_date(date: DicomDate) -> DicomDateTime { + DicomDateTime { + date, + time: None, + offset: None, + } + } + + /** + * Constructs a new `DicomDateTime` from a `DicomDate` and `DicomTime`, + * providing that `DicomDate` is precise. + */ + pub fn from_date_and_time(date: DicomDate, time: DicomTime) -> Result { + if date.is_precise() { + Ok(DicomDateTime { + date, + time: Some(time), + offset: None, + }) + } else { + DateTimeFromPartialsSnafu { + value: date.precision(), + } + .fail() } } @@ -590,7 +620,7 @@ impl DicomDateTime { * Constructs a new `DicomDateTime` from a `DicomDate`, `DicomTime` and a given `FixedOffset`, * providing that `DicomDate` is precise. */ - pub fn from_date_and_time( + pub fn from_date_and_time_tz( date: DicomDate, time: DicomTime, offset: FixedOffset, @@ -599,7 +629,7 @@ impl DicomDateTime { Ok(DicomDateTime { date, time: Some(time), - offset, + offset: Some(offset), }) } else { DateTimeFromPartialsSnafu { @@ -619,9 +649,14 @@ impl DicomDateTime { self.time.as_ref() } - /** Retrieves a refrence to the internal offset value */ - pub fn offset(&self) -> &FixedOffset { - &self.offset + /** Retrieves a refrence to the internal time-zone value, if present */ + pub fn time_zone(&self) -> Option<&FixedOffset> { + self.offset.as_ref() + } + + /** Returns true, if the `DicomDateTime` contains a time-zone */ + pub fn has_time_zone(&self) -> bool { + self.offset.is_some() } } @@ -660,7 +695,7 @@ impl TryFrom<&DateTime> for DicomDateTime { (second, microsecond) }; - DicomDateTime::from_date_and_time( + DicomDateTime::from_date_and_time_tz( DicomDate::from_ymd(year, month, day)?, DicomTime::from_hms_micro(hour, minute, second, microsecond)?, *dt.offset(), @@ -668,17 +703,59 @@ impl TryFrom<&DateTime> for DicomDateTime { } } +impl TryFrom<&NaiveDateTime> for DicomDateTime { + type Error = Error; + fn try_from(dt: &NaiveDateTime) -> Result { + let year: u16 = dt.year().try_into().context(ConversionSnafu { + value: dt.year().to_string(), + component: DateComponent::Year, + })?; + let month: u8 = dt.month().try_into().context(ConversionSnafu { + value: dt.month().to_string(), + component: DateComponent::Month, + })?; + let day: u8 = dt.day().try_into().context(ConversionSnafu { + value: dt.day().to_string(), + component: DateComponent::Day, + })?; + let hour: u8 = dt.hour().try_into().context(ConversionSnafu { + value: dt.hour().to_string(), + component: DateComponent::Hour, + })?; + let minute: u8 = dt.minute().try_into().context(ConversionSnafu { + value: dt.minute().to_string(), + component: DateComponent::Minute, + })?; + let second: u8 = dt.second().try_into().context(ConversionSnafu { + value: dt.second().to_string(), + component: DateComponent::Second, + })?; + let microsecond = dt.nanosecond() / 1000; + // leap second correction: convert (59, 1_000_000 + x) to (60, x) + let (second, microsecond) = if microsecond >= 1_000_000 && second == 59 { + (60, microsecond - 1_000_000) + } else { + (second, microsecond) + }; + + DicomDateTime::from_date_and_time( + DicomDate::from_ymd(year, month, day)?, + DicomTime::from_hms_micro(hour, minute, second, microsecond)?, + ) + } +} + impl fmt::Display for DicomDateTime { fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result { - // as DicomDateTime always contains a FixedOffset, it will always be written, - // even if it is zero. - // For absolute consistency between deserialized and serialized date-times, - // DicomDateTime would have to contain Some(FixedOffset)/None if none was parsed. - // storing an Option is useless, since a FixedOffset has to be available - // for conversion into chrono values match self.time { - None => write!(frm, "{} {}", self.date, self.offset), - Some(time) => write!(frm, "{} {} {}", self.date, time, self.offset), + None => match self.offset { + Some(offset) => write!(frm, "{} {}", self.date, offset), + None => write!(frm, "{}", self.date), + }, + Some(time) => match self.offset { + Some(offset) => write!(frm, "{} {} {}", self.date, time, offset), + None => write!(frm, "{} {}", self.date, time), + }, } } } @@ -686,8 +763,14 @@ impl fmt::Display for DicomDateTime { impl fmt::Debug for DicomDateTime { fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result { match self.time { - None => write!(frm, "{:?} {:?}", self.date, self.offset), - Some(time) => write!(frm, "{:?} {:?} {}", self.date, time, self.offset), + None => match self.offset { + Some(offset) => write!(frm, "{:?} {}", self.date, offset), + None => write!(frm, "{:?}", self.date), + }, + Some(time) => match self.offset { + Some(offset) => write!(frm, "{:?} {:?} {}", self.date, time, offset), + None => write!(frm, "{:?} {:?}", self.date, time), + }, } } } @@ -776,17 +859,23 @@ impl DicomDateTime { */ pub fn to_encoded(&self) -> String { match self.time { - Some(time) => format!( - "{}{}{}", - self.date.to_encoded(), - time.to_encoded(), - self.offset.to_string().replace(':', "") - ), - None => format!( - "{}{}", - self.date.to_encoded(), - self.offset.to_string().replace(':', "") - ), + Some(time) => match self.offset { + Some(offset) => format!( + "{}{}{}", + self.date.to_encoded(), + time.to_encoded(), + offset.to_string().replace(':', "") + ), + None => format!("{}{}", self.date.to_encoded(), time.to_encoded()), + }, + None => match self.offset { + Some(offset) => format!( + "{}{}", + self.date.to_encoded(), + offset.to_string().replace(':', "") + ), + None => format!("{}", self.date.to_encoded()), + }, } } } @@ -1031,7 +1120,7 @@ mod tests { DicomDateTime { date: DicomDate::from_ymd(2020, 2, 29).unwrap(), time: None, - offset: default_offset + offset: Some(default_offset) } ); @@ -1110,7 +1199,7 @@ mod tests { DicomDateTime { date: DicomDate::from_ymd(2020, 2, 29).unwrap(), time: Some(DicomTime::from_hms_micro(23, 59, 59, 999_999).unwrap()), - offset: default_offset + offset: Some(default_offset) } ); @@ -1128,7 +1217,7 @@ mod tests { DicomDateTime { date: DicomDate::from_ymd(2020, 2, 29).unwrap(), time: Some(DicomTime::from_hms_micro(23, 59, 59, 0).unwrap()), - offset: default_offset + offset: Some(default_offset) } ); @@ -1147,7 +1236,7 @@ mod tests { DicomDateTime { date: DicomDate::from_ymd(2023, 12, 31).unwrap(), time: Some(DicomTime::from_hms_micro(23, 59, 60, 0).unwrap()), - offset: default_offset + offset: Some(default_offset) } ); diff --git a/core/src/value/range.rs b/core/src/value/range.rs index 902d5b408..9ec463fe4 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -1,7 +1,7 @@ //! Handling of date, time, date-time ranges. Needed for range matching. //! Parsing into ranges happens via partial precision structures (DicomDate, DicomTime, //! DicomDatime) so ranges can handle null components in date, time, date-time values. -use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone}; +use chrono::{DateTime, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone}; use snafu::{Backtrace, OptionExt, ResultExt, Snafu}; use crate::value::deserialize::{ @@ -57,6 +57,25 @@ pub enum Error { f: u32, backtrace: Backtrace, }, + #[snafu(display( + "Date-time does not contain time-zone, cannot convert to time-zone aware value" + ))] + NoTimeZone { backtrace: Backtrace }, + #[snafu(display( + "Failed to convert to a time-zone aware date-time value with ambiguous results: {t1}, {t2} " + ))] + DateTimeAmbiguous { + t1: DateTime, + t2: DateTime, + }, + #[snafu(display( + "Failed to convert to a time-zone aware date-time value: Given local time representation is invalid" + ))] + DateTimeInvalid { backtrace: Backtrace }, + #[snafu(display( + "Trying to convert a time-zone aware date-time value to a time-zone unaware value" + ))] + DateTimeTzAware { backtrace: Backtrace }, } type Result = std::result::Result; @@ -253,7 +272,7 @@ impl AsRange for DicomTime { } impl AsRange for DicomDateTime { - type Item = DateTime; + type Item = NaiveDateTime; type Range = DateTimeRange; fn earliest(&self) -> Result> { let date = self.date().earliest()?; @@ -319,9 +338,46 @@ impl DicomTime { } impl DicomDateTime { - /// Retrieves a `chrono::DateTime` if value is precise. + /// Retrieves a `chrono::DateTime` by converting the internal time-zone naive date-time representation + /// to a time-zone aware representation. + /// It the value does not store a time-zone or the date-time value is not precise or the conversion leads to ambiguous results, + /// it fails. + /// To inspect the possibly ambiguous results of this conversion, see `to_chrono_local_result` pub fn to_chrono_datetime(self) -> Result> { - // tweak here, if full DicomTime precision req. proves impractical + if let Some(offset) = self.time_zone() { + match offset.from_local_datetime(&self.exact()?) { + LocalResult::Single(date_time) => Ok(date_time), + LocalResult::Ambiguous(t1, t2) => DateTimeAmbiguousSnafu { t1, t2 }.fail(), + LocalResult::None => DateTimeInvalidSnafu.fail(), + } + } else { + NoTimeZoneSnafu.fail() + } + } + + /// Retrieves a `chrono::LocalResult` by converting the internal time-zone naive date-time representation + /// to a time-zone aware representation. + /// It the value does not store a time-zone or the date-time value is not precise, it fails. + pub fn to_chrono_local_result(self) -> Result>> { + if let Some(offset) = self.time_zone() { + Ok(offset.from_local_datetime(&self.exact()?)) + } else { + NoTimeZoneSnafu.fail() + } + } + + /// Retrieves a `chrono::NaiveDateTime`. If the internal date-time value is not precise or + /// it is time-zone aware, this method will fail. + pub fn to_naive_datetime(&self) -> Result{ + if self.time_zone().is_some(){ + + } + self.exact() + } + + /// Retrieves a `chrono::NaiveDateTime` it the value is precise. This method will work even if the + /// date-time value is time-zone aware and thus expects that you know what you are doing. + pub fn to_naive_datetime_ignore_timezone(&self) -> Result{ self.exact() } } From 7df0599463cdb2905ee619113bee83ea801fa3e4 Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Mon, 5 Feb 2024 01:02:58 +0100 Subject: [PATCH 02/23] - doc tests need fixing --- core/src/header.rs | 14 +- core/src/value/deserialize.rs | 119 +++--- core/src/value/mod.rs | 25 +- core/src/value/partial.rs | 112 +++--- core/src/value/primitive.rs | 216 ++++++----- core/src/value/range.rs | 682 ++++++++++++++++++++++++---------- core/src/value/serialize.rs | 11 +- dump/src/lib.rs | 2 +- object/src/mem.rs | 4 +- parser/src/stateful/decode.rs | 5 +- 10 files changed, 749 insertions(+), 441 deletions(-) diff --git a/core/src/header.rs b/core/src/header.rs index 97f4f43b7..e3763adb7 100644 --- a/core/src/header.rs +++ b/core/src/header.rs @@ -503,11 +503,8 @@ where /// /// Returns an error if the value is not primitive. /// - pub fn to_datetime( - &self, - default_offset: FixedOffset, - ) -> Result { - self.value().to_datetime(default_offset) + pub fn to_datetime(&self) -> Result { + self.value().to_datetime() } /// Retrieve and convert the primitive value into a sequence of date-times. @@ -517,11 +514,8 @@ where /// /// Returns an error if the value is not primitive. /// - pub fn to_multi_datetime( - &self, - default_offset: FixedOffset, - ) -> Result, ConvertValueError> { - self.value().to_multi_datetime(default_offset) + pub fn to_multi_datetime(&self) -> Result, ConvertValueError> { + self.value().to_multi_datetime() } /// Retrieve the items stored in a sequence value. diff --git a/core/src/value/deserialize.rs b/core/src/value/deserialize.rs index 5f5ceed4d..9ddafd7a3 100644 --- a/core/src/value/deserialize.rs +++ b/core/src/value/deserialize.rs @@ -337,6 +337,10 @@ where * For DateTime with missing components, or if exact second fraction accuracy needs to be preserved, use `parse_datetime_partial`. */ +#[deprecated( + note = "As time-zone aware an naive DicomDateTime was introduced, this method could only access the time-zone + aware values, which would lead to confusing behavior." +)] pub fn parse_datetime(buf: &[u8], dt_utc_offset: FixedOffset) -> Result> { let date = parse_date(buf)?; let buf = &buf[8..]; @@ -377,9 +381,8 @@ pub fn parse_datetime(buf: &[u8], dt_utc_offset: FixedOffset) -> Result Result { +pub fn parse_datetime_partial(buf: &[u8]) -> Result { let (date, rest) = parse_date_partial(buf)?; let (time, buf) = match parse_time_partial(rest) { @@ -387,8 +390,8 @@ pub fn parse_datetime_partial(buf: &[u8], dt_utc_offset: FixedOffset) -> Result< Err(_) => (None, rest), }; - let offset = match buf.len() { - 0 => dt_utc_offset, + let time_zone = match buf.len() { + 0 => None, len if len > 4 => { let tz_sign = buf[0]; let buf = &buf[1..]; @@ -398,13 +401,17 @@ pub fn parse_datetime_partial(buf: &[u8], dt_utc_offset: FixedOffset) -> Result< match tz_sign { b'+' => { check_component(DateComponent::UtcEast, &s).context(InvalidComponentSnafu)?; - FixedOffset::east_opt(s as i32) - .context(SecsOutOfBoundsSnafu { secs: s as i32 })? + Some( + FixedOffset::east_opt(s as i32) + .context(SecsOutOfBoundsSnafu { secs: s as i32 })?, + ) } b'-' => { check_component(DateComponent::UtcWest, &s).context(InvalidComponentSnafu)?; - FixedOffset::west_opt(s as i32) - .context(SecsOutOfBoundsSnafu { secs: s as i32 })? + Some( + FixedOffset::west_opt(s as i32) + .context(SecsOutOfBoundsSnafu { secs: s as i32 })?, + ) } c => return InvalidTimeZoneSignTokenSnafu { value: c }.fail(), } @@ -412,11 +419,16 @@ pub fn parse_datetime_partial(buf: &[u8], dt_utc_offset: FixedOffset) -> Result< _ => return UnexpectedEndOfElementSnafu.fail(), }; - match time { - Some(tm) => { - DicomDateTime::from_date_and_time(date, tm, offset).context(InvalidDateTimeSnafu) - } - None => Ok(DicomDateTime::from_date(date, offset)), + match time_zone { + Some(time_zone) => match time { + Some(tm) => DicomDateTime::from_date_and_time_with_time_zone(date, tm, time_zone) + .context(InvalidDateTimeSnafu), + None => Ok(DicomDateTime::from_date_with_time_zone(date, time_zone)), + }, + None => match time { + Some(tm) => DicomDateTime::from_date_and_time(date, tm).context(InvalidDateTimeSnafu), + None => Ok(DicomDateTime::from_date(date)), + }, } } @@ -955,39 +967,36 @@ mod tests { fn test_parse_datetime_partial() { let default_offset = FixedOffset::east_opt(0).unwrap(); assert_eq!( - parse_datetime_partial(b"20171130101010.204", default_offset).unwrap(), + parse_datetime_partial(b"20171130101010.204").unwrap(), DicomDateTime::from_date_and_time( DicomDate::from_ymd(2017, 11, 30).unwrap(), DicomTime::from_hmsf(10, 10, 10, 204, 3).unwrap(), - default_offset ) .unwrap() ); assert_eq!( - parse_datetime_partial(b"20171130101010", default_offset).unwrap(), + parse_datetime_partial(b"20171130101010").unwrap(), DicomDateTime::from_date_and_time( DicomDate::from_ymd(2017, 11, 30).unwrap(), - DicomTime::from_hms(10, 10, 10).unwrap(), - default_offset + DicomTime::from_hms(10, 10, 10).unwrap() ) .unwrap() ); assert_eq!( - parse_datetime_partial(b"2017113023", default_offset).unwrap(), + parse_datetime_partial(b"2017113023").unwrap(), DicomDateTime::from_date_and_time( DicomDate::from_ymd(2017, 11, 30).unwrap(), - DicomTime::from_h(23).unwrap(), - default_offset + DicomTime::from_h(23).unwrap() ) .unwrap() ); assert_eq!( - parse_datetime_partial(b"201711", default_offset).unwrap(), - DicomDateTime::from_date(DicomDate::from_ym(2017, 11).unwrap(), default_offset) + parse_datetime_partial(b"201711").unwrap(), + DicomDateTime::from_date(DicomDate::from_ym(2017, 11).unwrap()) ); assert_eq!( - parse_datetime_partial(b"20171130101010.204+0535", default_offset).unwrap(), - DicomDateTime::from_date_and_time( + parse_datetime_partial(b"20171130101010.204+0535").unwrap(), + DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_ymd(2017, 11, 30).unwrap(), DicomTime::from_hmsf(10, 10, 10, 204, 3).unwrap(), FixedOffset::east_opt(5 * 3600 + 35 * 60).unwrap() @@ -995,8 +1004,8 @@ mod tests { .unwrap() ); assert_eq!( - parse_datetime_partial(b"20171130101010+0535", default_offset).unwrap(), - DicomDateTime::from_date_and_time( + parse_datetime_partial(b"20171130101010+0535").unwrap(), + DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_ymd(2017, 11, 30).unwrap(), DicomTime::from_hms(10, 10, 10).unwrap(), FixedOffset::east_opt(5 * 3600 + 35 * 60).unwrap() @@ -1004,8 +1013,8 @@ mod tests { .unwrap() ); assert_eq!( - parse_datetime_partial(b"2017113010+0535", default_offset).unwrap(), - DicomDateTime::from_date_and_time( + parse_datetime_partial(b"2017113010+0535").unwrap(), + DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_ymd(2017, 11, 30).unwrap(), DicomTime::from_h(10).unwrap(), FixedOffset::east_opt(5 * 3600 + 35 * 60).unwrap() @@ -1013,22 +1022,22 @@ mod tests { .unwrap() ); assert_eq!( - parse_datetime_partial(b"20171130-0135", default_offset).unwrap(), - DicomDateTime::from_date( + parse_datetime_partial(b"20171130-0135").unwrap(), + DicomDateTime::from_date_with_time_zone( DicomDate::from_ymd(2017, 11, 30).unwrap(), FixedOffset::west_opt(1 * 3600 + 35 * 60).unwrap() ) ); assert_eq!( - parse_datetime_partial(b"201711-0135", default_offset).unwrap(), - DicomDateTime::from_date( + parse_datetime_partial(b"201711-0135").unwrap(), + DicomDateTime::from_date_with_time_zone( DicomDate::from_ym(2017, 11).unwrap(), FixedOffset::west_opt(1 * 3600 + 35 * 60).unwrap() ) ); assert_eq!( - parse_datetime_partial(b"2017-0135", default_offset).unwrap(), - DicomDateTime::from_date( + parse_datetime_partial(b"2017-0135").unwrap(), + DicomDateTime::from_date_with_time_zone( DicomDate::from_y(2017).unwrap(), FixedOffset::west_opt(1 * 3600 + 35 * 60).unwrap() ) @@ -1036,37 +1045,37 @@ mod tests { // West UTC offset out of range assert!(matches!( - parse_datetime_partial(b"20200101-1201", default_offset), + parse_datetime_partial(b"20200101-1201"), Err(Error::InvalidComponent { .. }) )); // East UTC offset out of range assert!(matches!( - parse_datetime_partial(b"20200101+1401", default_offset), + parse_datetime_partial(b"20200101+1401"), Err(Error::InvalidComponent { .. }) )); assert!(matches!( - parse_datetime_partial(b"xxxx0229101010.204", default_offset), + parse_datetime_partial(b"xxxx0229101010.204"), Err(Error::InvalidNumberToken { .. }) )); - assert!(parse_datetime_partial(b"", default_offset).is_err()); - assert!(parse_datetime_partial(&[0x00_u8; 8], default_offset).is_err()); - assert!(parse_datetime_partial(&[0xFF_u8; 8], default_offset).is_err()); - assert!(parse_datetime_partial(&[b'0'; 8], default_offset).is_err()); - assert!(parse_datetime_partial(&[b' '; 8], default_offset).is_err()); - assert!(parse_datetime_partial(b"nope", default_offset).is_err()); - assert!(parse_datetime_partial(b"2015dec", default_offset).is_err()); - assert!(parse_datetime_partial(b"20151231162945.", default_offset).is_err()); - assert!(parse_datetime_partial(b"20151130161445+", default_offset).is_err()); - assert!(parse_datetime_partial(b"20151130161445+----", default_offset).is_err()); - assert!(parse_datetime_partial(b"20151130161445. ", default_offset).is_err()); - assert!(parse_datetime_partial(b"20151130161445. +0000", default_offset).is_err()); - assert!(parse_datetime_partial(b"20100423164000.001+3", default_offset).is_err()); - assert!(parse_datetime_partial(b"200809112945*1000", default_offset).is_err()); - assert!(parse_datetime_partial(b"20171130101010.204+1", default_offset).is_err()); - assert!(parse_datetime_partial(b"20171130101010.204+01", default_offset).is_err()); - assert!(parse_datetime_partial(b"20171130101010.204+011", default_offset).is_err()); + assert!(parse_datetime_partial(b"").is_err()); + assert!(parse_datetime_partial(&[0x00_u8; 8]).is_err()); + assert!(parse_datetime_partial(&[0xFF_u8; 8]).is_err()); + assert!(parse_datetime_partial(&[b'0'; 8]).is_err()); + assert!(parse_datetime_partial(&[b' '; 8]).is_err()); + assert!(parse_datetime_partial(b"nope").is_err()); + assert!(parse_datetime_partial(b"2015dec").is_err()); + assert!(parse_datetime_partial(b"20151231162945.").is_err()); + assert!(parse_datetime_partial(b"20151130161445+").is_err()); + assert!(parse_datetime_partial(b"20151130161445+----").is_err()); + assert!(parse_datetime_partial(b"20151130161445. ").is_err()); + assert!(parse_datetime_partial(b"20151130161445. +0000").is_err()); + assert!(parse_datetime_partial(b"20100423164000.001+3").is_err()); + assert!(parse_datetime_partial(b"200809112945*1000").is_err()); + assert!(parse_datetime_partial(b"20171130101010.204+1").is_err()); + assert!(parse_datetime_partial(b"20171130101010.204+01").is_err()); + assert!(parse_datetime_partial(b"20171130101010.204+011").is_err()); } } diff --git a/core/src/value/mod.rs b/core/src/value/mod.rs index 29fcbca9b..22adc97b9 100644 --- a/core/src/value/mod.rs +++ b/core/src/value/mod.rs @@ -252,7 +252,7 @@ impl Value { /// Shorten this value by removing trailing elements /// to fit the given limit. - /// + /// /// On primitive values, /// elements are counted by the number of individual value items /// (note that bytes in a [`PrimitiveValue::U8`] @@ -578,12 +578,9 @@ where /// If the value is a primitive, it will be converted into /// a `DateTime` as described in [`PrimitiveValue::to_datetime`]. /// - pub fn to_datetime( - &self, - default_offset: FixedOffset, - ) -> Result { + pub fn to_datetime(&self) -> Result { match self { - Value::Primitive(v) => v.to_datetime(default_offset), + Value::Primitive(v) => v.to_datetime(), _ => Err(ConvertValueError { requested: "DicomDateTime", original: self.value_type(), @@ -597,12 +594,9 @@ where /// If the value is a primitive, it will be converted into /// a vector of `DicomDateTime` as described in [`PrimitiveValue::to_multi_datetime`]. /// - pub fn to_multi_datetime( - &self, - default_offset: FixedOffset, - ) -> Result, ConvertValueError> { + pub fn to_multi_datetime(&self) -> Result, ConvertValueError> { match self { - Value::Primitive(v) => v.to_multi_datetime(default_offset), + Value::Primitive(v) => v.to_multi_datetime(), _ => Err(ConvertValueError { requested: "DicomDateTime", original: self.value_type(), @@ -648,12 +642,9 @@ where /// If the value is a primitive, it will be converted into /// a `DateTimeRange` as described in [`PrimitiveValue::to_datetime_range`]. /// - pub fn to_datetime_range( - &self, - offset: FixedOffset, - ) -> Result { + pub fn to_datetime_range(&self) -> Result { match self { - Value::Primitive(v) => v.to_datetime_range(offset), + Value::Primitive(v) => v.to_datetime_range(), _ => Err(ConvertValueError { requested: "DateTimeRange", original: self.value_type(), @@ -1043,7 +1034,7 @@ impl

PixelFragmentSequence

{ /// Shorten this sequence by removing trailing fragments /// to fit the given limit. - /// + /// /// Note that this operations does not affect the basic offset table. #[inline] pub fn truncate(&mut self, limit: usize) { diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 2727deb6d..7d3f60654 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -177,9 +177,9 @@ enum DicomTimeImpl { /// where some date or time components may be missing. /// /// `DicomDateTime` is always internally represented by a [DicomDate] -/// and optionally by a [DicomTime]. +/// and optionally by a [DicomTime] and a timezone [FixedOffset]. /// -/// It implements [AsRange] trait and also holds a [FixedOffset] value, from which corresponding +/// It implements [AsRange] trait and optionally holds a [FixedOffset] value, from which corresponding /// [datetime][DateTime] values can be retrieved. /// # Example /// ``` @@ -228,7 +228,7 @@ enum DicomTimeImpl { pub struct DicomDateTime { date: DicomDate, time: Option, - offset: Option, + time_zone: Option, } /** @@ -576,13 +576,13 @@ impl fmt::Debug for DicomTime { impl DicomDateTime { /** - * Constructs a new `DicomDateTime` from a `DicomDate` and a given `FixedOffset`. + * Constructs a new `DicomDateTime` from a `DicomDate` and a timezone `FixedOffset`. */ - pub fn from_date_tz(date: DicomDate, offset: FixedOffset) -> DicomDateTime { + pub fn from_date_with_time_zone(date: DicomDate, time_zone: FixedOffset) -> DicomDateTime { DicomDateTime { date, time: None, - offset: Some(offset), + time_zone: Some(time_zone), } } @@ -593,12 +593,12 @@ impl DicomDateTime { DicomDateTime { date, time: None, - offset: None, + time_zone: None, } } /** - * Constructs a new `DicomDateTime` from a `DicomDate` and `DicomTime`, + * Constructs a new `DicomDateTime` from a `DicomDate` and a `DicomTime`, * providing that `DicomDate` is precise. */ pub fn from_date_and_time(date: DicomDate, time: DicomTime) -> Result { @@ -606,7 +606,7 @@ impl DicomDateTime { Ok(DicomDateTime { date, time: Some(time), - offset: None, + time_zone: None, }) } else { DateTimeFromPartialsSnafu { @@ -617,19 +617,19 @@ impl DicomDateTime { } /** - * Constructs a new `DicomDateTime` from a `DicomDate`, `DicomTime` and a given `FixedOffset`, + * Constructs a new `DicomDateTime` from a `DicomDate`, `DicomTime` and a timezone `FixedOffset`, * providing that `DicomDate` is precise. */ - pub fn from_date_and_time_tz( + pub fn from_date_and_time_with_time_zone( date: DicomDate, time: DicomTime, - offset: FixedOffset, + time_zone: FixedOffset, ) -> Result { if date.is_precise() { Ok(DicomDateTime { date, time: Some(time), - offset: Some(offset), + time_zone: Some(time_zone), }) } else { DateTimeFromPartialsSnafu { @@ -651,12 +651,12 @@ impl DicomDateTime { /** Retrieves a refrence to the internal time-zone value, if present */ pub fn time_zone(&self) -> Option<&FixedOffset> { - self.offset.as_ref() + self.time_zone.as_ref() } /** Returns true, if the `DicomDateTime` contains a time-zone */ pub fn has_time_zone(&self) -> bool { - self.offset.is_some() + self.time_zone.is_some() } } @@ -695,7 +695,7 @@ impl TryFrom<&DateTime> for DicomDateTime { (second, microsecond) }; - DicomDateTime::from_date_and_time_tz( + DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_ymd(year, month, day)?, DicomTime::from_hms_micro(hour, minute, second, microsecond)?, *dt.offset(), @@ -748,11 +748,11 @@ impl TryFrom<&NaiveDateTime> for DicomDateTime { impl fmt::Display for DicomDateTime { fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result { match self.time { - None => match self.offset { + None => match self.time_zone { Some(offset) => write!(frm, "{} {}", self.date, offset), None => write!(frm, "{}", self.date), }, - Some(time) => match self.offset { + Some(time) => match self.time_zone { Some(offset) => write!(frm, "{} {} {}", self.date, time, offset), None => write!(frm, "{} {}", self.date, time), }, @@ -763,11 +763,11 @@ impl fmt::Display for DicomDateTime { impl fmt::Debug for DicomDateTime { fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result { match self.time { - None => match self.offset { + None => match self.time_zone { Some(offset) => write!(frm, "{:?} {}", self.date, offset), None => write!(frm, "{:?}", self.date), }, - Some(time) => match self.offset { + Some(time) => match self.time_zone { Some(offset) => write!(frm, "{:?} {:?} {}", self.date, time, offset), None => write!(frm, "{:?} {:?}", self.date, time), }, @@ -859,7 +859,7 @@ impl DicomDateTime { */ pub fn to_encoded(&self) -> String { match self.time { - Some(time) => match self.offset { + Some(time) => match self.time_zone { Some(offset) => format!( "{}{}{}", self.date.to_encoded(), @@ -868,7 +868,7 @@ impl DicomDateTime { ), None => format!("{}{}", self.date.to_encoded(), time.to_encoded()), }, - None => match self.offset { + None => match self.time_zone { Some(offset) => format!( "{}{}", self.date.to_encoded(), @@ -882,6 +882,8 @@ impl DicomDateTime { #[cfg(test)] mod tests { + use crate::value::range::PreciseDateTimeResult; + use super::*; use chrono::{NaiveDateTime, TimeZone}; @@ -1116,31 +1118,37 @@ mod tests { fn test_dicom_datetime() { let default_offset = FixedOffset::east_opt(0).unwrap(); assert_eq!( - DicomDateTime::from_date(DicomDate::from_ymd(2020, 2, 29).unwrap(), default_offset), + DicomDateTime::from_date_with_time_zone( + DicomDate::from_ymd(2020, 2, 29).unwrap(), + default_offset + ), DicomDateTime { date: DicomDate::from_ymd(2020, 2, 29).unwrap(), time: None, - offset: Some(default_offset) + time_zone: Some(default_offset) } ); assert_eq!( - DicomDateTime::from_date(DicomDate::from_ym(2020, 2).unwrap(), default_offset) - .earliest() - .unwrap(), - FixedOffset::east_opt(0) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2020, 2, 1).unwrap(), - NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() - )) - .unwrap() + DicomDateTime::from_date( + DicomDate::from_ym(2020, 2).unwrap() + ) + .earliest() + .unwrap(), + PreciseDateTimeResult::Naive(NaiveDateTime::new( + NaiveDate::from_ymd_opt(2020, 2, 1).unwrap(), + NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() + )) ); assert_eq!( - DicomDateTime::from_date(DicomDate::from_ym(2020, 2).unwrap(), default_offset) - .latest() - .unwrap(), + DicomDateTime::from_date_with_time_zone( + DicomDate::from_ym(2020, 2).unwrap(), + default_offset + ) + .latest() + .unwrap(), + PreciseDateTimeResult::WithTimeZone( FixedOffset::east_opt(0) .unwrap() .from_local_datetime(&NaiveDateTime::new( @@ -1148,10 +1156,12 @@ mod tests { NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() )) .unwrap() + ) + ); assert_eq!( - DicomDateTime::from_date_and_time( + DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_ymd(2020, 2, 29).unwrap(), DicomTime::from_hmsf(23, 59, 59, 10, 2).unwrap(), default_offset @@ -1159,6 +1169,7 @@ mod tests { .unwrap() .earliest() .unwrap(), + PreciseDateTimeResult::WithTimeZone( FixedOffset::east_opt(0) .unwrap() .from_local_datetime(&NaiveDateTime::new( @@ -1166,9 +1177,11 @@ mod tests { NaiveTime::from_hms_micro_opt(23, 59, 59, 100_000).unwrap() )) .unwrap() + ) + ); assert_eq!( - DicomDateTime::from_date_and_time( + DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_ymd(2020, 2, 29).unwrap(), DicomTime::from_hmsf(23, 59, 59, 10, 2).unwrap(), default_offset @@ -1176,6 +1189,7 @@ mod tests { .unwrap() .latest() .unwrap(), + PreciseDateTimeResult::WithTimeZone( FixedOffset::east_opt(0) .unwrap() .from_local_datetime(&NaiveDateTime::new( @@ -1183,6 +1197,7 @@ mod tests { NaiveTime::from_hms_micro_opt(23, 59, 59, 109_999).unwrap() )) .unwrap() + ) ); assert_eq!( @@ -1199,7 +1214,7 @@ mod tests { DicomDateTime { date: DicomDate::from_ymd(2020, 2, 29).unwrap(), time: Some(DicomTime::from_hms_micro(23, 59, 59, 999_999).unwrap()), - offset: Some(default_offset) + time_zone: Some(default_offset) } ); @@ -1217,7 +1232,7 @@ mod tests { DicomDateTime { date: DicomDate::from_ymd(2020, 2, 29).unwrap(), time: Some(DicomTime::from_hms_micro(23, 59, 59, 0).unwrap()), - offset: Some(default_offset) + time_zone: Some(default_offset) } ); @@ -1236,18 +1251,21 @@ mod tests { DicomDateTime { date: DicomDate::from_ymd(2023, 12, 31).unwrap(), time: Some(DicomTime::from_hms_micro(23, 59, 60, 0).unwrap()), - offset: Some(default_offset) + time_zone: Some(default_offset) } ); assert!(matches!( - DicomDateTime::from_date(DicomDate::from_ymd(2021, 2, 29).unwrap(), default_offset) - .earliest(), + DicomDateTime::from_date_with_time_zone( + DicomDate::from_ymd(2021, 2, 29).unwrap(), + default_offset + ) + .earliest(), Err(crate::value::range::Error::InvalidDate { .. }) )); assert!(matches!( - DicomDateTime::from_date_and_time( + DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_ym(2020, 2).unwrap(), DicomTime::from_hms_milli(23, 59, 59, 999).unwrap(), default_offset @@ -1258,7 +1276,7 @@ mod tests { }) )); assert!(matches!( - DicomDateTime::from_date_and_time( + DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_y(1).unwrap(), DicomTime::from_hms_micro(23, 59, 59, 10).unwrap(), default_offset @@ -1270,7 +1288,7 @@ mod tests { )); assert!(matches!( - DicomDateTime::from_date_and_time( + DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_ymd(2000, 1, 1).unwrap(), DicomTime::from_hms_milli(23, 59, 59, 10).unwrap(), default_offset diff --git a/core/src/value/primitive.rs b/core/src/value/primitive.rs index 3cd9c3968..2b61cc95c 100644 --- a/core/src/value/primitive.rs +++ b/core/src/value/primitive.rs @@ -516,9 +516,10 @@ impl PrimitiveValue { Some(time) => PrimitiveValue::tm_byte_len(time), None => 0, } - + 5 - // always return length of UTC offset, as current impl Display for DicomDateTime - // always writes the offset, even if it is zero + + match datetime.has_time_zone() { + true => 5, + false => 0, + } } /// Convert the primitive value into a string representation. @@ -1907,6 +1908,10 @@ impl PrimitiveValue { /// Retrieve a single `chrono::NaiveDate` from this value. /// + /// Please note, that this is a shortcut to obtain a usable date from a primitive value. + /// As per standard, the stored value might not be precise. It is highly recommended to + /// use [`.to_date()`](PrimitiveValue::to_date) as the only way to obtain dates. + /// /// If the value is already represented as a precise `DicomDate`, it is converted /// to a `NaiveDate` value. It fails for imprecise values. /// If the value is a string or sequence of strings, @@ -1995,6 +2000,10 @@ impl PrimitiveValue { /// Retrieve the full sequence of `chrono::NaiveDate`s from this value. /// + /// Please note, that this is a shortcut to obtain usable dates from a primitive value. + /// As per standard, the stored values might not be precise. It is highly recommended to + /// use [`.to_multi_date()`](PrimitiveValue::to_multi_date) as the only way to obtain dates. + /// /// If the value is already represented as a sequence of precise `DicomDate` values, /// it is converted. It fails for imprecise values. /// If the value is a string or sequence of strings, @@ -2250,6 +2259,10 @@ impl PrimitiveValue { /// Retrieve a single `chrono::NaiveTime` from this value. /// + /// Please note, that this is a shortcut to obtain a usable time from a primitive value. + /// As per standard, the stored value might not be precise. It is highly recommended to + /// use [`.to_time()`](PrimitiveValue::to_time) as the only way to obtain times. + /// /// If the value is represented as a precise `DicomTime`, /// it is converted to a `NaiveTime`. /// It fails for imprecise values, @@ -2261,9 +2274,6 @@ impl PrimitiveValue { /// first interpreted as an ASCII character string. /// Otherwise, the operation fails. /// - /// Users are advised that this method requires at least 1 out of 6 digits of the second - /// fraction .F to be present. Otherwise, the operation fails. - /// /// Partial precision times are handled by `DicomTime`, /// which can be retrieved by [`.to_time()`](PrimitiveValue::to_time). /// @@ -2336,6 +2346,10 @@ impl PrimitiveValue { /// Retrieve the full sequence of `chrono::NaiveTime`s from this value. /// + /// Please note, that this is a shortcut to obtain a usable time from a primitive value. + /// As per standard, the stored values might not be precise. It is highly recommended to + /// use [`.to_multi_time()`](PrimitiveValue::to_multi_time) as the only way to obtain times. + /// /// If the value is already represented as a sequence of precise `DicomTime` values, /// it is converted to a sequence of `NaiveTime` values. It fails for imprecise values. /// If the value is a string or sequence of strings, @@ -2686,19 +2700,15 @@ impl PrimitiveValue { /// # Ok(()) /// # } /// ``` + #[deprecated( + note = "As time-zone aware an naive DicomDateTime was introduced, this method could only access the time-zone + aware values, which would lead to confusing behavior." + )] pub fn to_chrono_datetime( &self, default_offset: FixedOffset, ) -> Result, ConvertValueError> { match self { - PrimitiveValue::DateTime(v) if !v.is_empty() => v[0] - .to_chrono_datetime() - .context(ParseDateTimeRangeSnafu) - .map_err(|err| ConvertValueError { - requested: "DateTime", - original: self.value_type(), - cause: Some(err), - }), PrimitiveValue::Str(s) => { super::deserialize::parse_datetime(s.trim_end().as_bytes(), default_offset) .context(ParseDateTimeSnafu) @@ -2794,21 +2804,15 @@ impl PrimitiveValue { /// # Ok(()) /// # } /// ``` + #[deprecated( + note = "As time-zone aware an naive DicomDateTime was introduced, this method could only access the time-zone + aware values, which would lead to confusing behavior." + )] pub fn to_multi_chrono_datetime( &self, default_offset: FixedOffset, ) -> Result>, ConvertValueError> { match self { - PrimitiveValue::DateTime(v) if !v.is_empty() => v - .into_iter() - .map(|dt| dt.to_chrono_datetime()) - .collect::, _>>() - .context(ParseDateTimeRangeSnafu) - .map_err(|err| ConvertValueError { - requested: "DateTime", - original: self.value_type(), - cause: Some(err), - }), PrimitiveValue::Str(s) => { super::deserialize::parse_datetime(s.trim_end().as_bytes(), default_offset) .map(|date| vec![date]) @@ -2919,14 +2923,11 @@ impl PrimitiveValue { /// # Ok(()) /// # } /// ``` - pub fn to_datetime( - &self, - default_offset: FixedOffset, - ) -> Result { + pub fn to_datetime(&self) -> Result { match self { PrimitiveValue::DateTime(v) if !v.is_empty() => Ok(v[0]), PrimitiveValue::Str(s) => { - super::deserialize::parse_datetime_partial(s.trim_end().as_bytes(), default_offset) + super::deserialize::parse_datetime_partial(s.trim_end().as_bytes()) .context(ParseDateTimeSnafu) .map_err(|err| ConvertValueError { requested: "DicomDateTime", @@ -2936,17 +2937,6 @@ impl PrimitiveValue { } PrimitiveValue::Strs(s) => super::deserialize::parse_datetime_partial( s.first().map(|s| s.trim_end().as_bytes()).unwrap_or(&[]), - default_offset, - ) - .context(ParseDateTimeSnafu) - .map_err(|err| ConvertValueError { - requested: "DicomDateTime", - original: self.value_type(), - cause: Some(err), - }), - PrimitiveValue::U8(bytes) => super::deserialize::parse_datetime_partial( - trim_last_whitespace(bytes), - default_offset, ) .context(ParseDateTimeSnafu) .map_err(|err| ConvertValueError { @@ -2954,6 +2944,15 @@ impl PrimitiveValue { original: self.value_type(), cause: Some(err), }), + PrimitiveValue::U8(bytes) => { + super::deserialize::parse_datetime_partial(trim_last_whitespace(bytes)) + .context(ParseDateTimeSnafu) + .map_err(|err| ConvertValueError { + requested: "DicomDateTime", + original: self.value_type(), + cause: Some(err), + }) + } _ => Err(ConvertValueError { requested: "DicomDateTime", original: self.value_type(), @@ -2964,14 +2963,11 @@ impl PrimitiveValue { /// Retrieve the full sequence of `DicomDateTime`s from this value. /// - pub fn to_multi_datetime( - &self, - default_offset: FixedOffset, - ) -> Result, ConvertValueError> { + pub fn to_multi_datetime(&self) -> Result, ConvertValueError> { match self { PrimitiveValue::DateTime(v) => Ok(v.to_vec()), PrimitiveValue::Str(s) => { - super::deserialize::parse_datetime_partial(s.trim_end().as_bytes(), default_offset) + super::deserialize::parse_datetime_partial(s.trim_end().as_bytes()) .map(|date| vec![date]) .context(ParseDateSnafu) .map_err(|err| ConvertValueError { @@ -2982,12 +2978,7 @@ impl PrimitiveValue { } PrimitiveValue::Strs(s) => s .into_iter() - .map(|s| { - super::deserialize::parse_datetime_partial( - s.trim_end().as_bytes(), - default_offset, - ) - }) + .map(|s| super::deserialize::parse_datetime_partial(s.trim_end().as_bytes())) .collect::, _>>() .context(ParseDateSnafu) .map_err(|err| ConvertValueError { @@ -2997,7 +2988,7 @@ impl PrimitiveValue { }), PrimitiveValue::U8(bytes) => trim_last_whitespace(bytes) .split(|c| *c == b'\\') - .map(|s| super::deserialize::parse_datetime_partial(s, default_offset)) + .map(|s| super::deserialize::parse_datetime_partial(s)) .collect::, _>>() .context(ParseDateSnafu) .map_err(|err| ConvertValueError { @@ -3205,23 +3196,17 @@ impl PrimitiveValue { /// # Ok(()) /// # } /// ``` - pub fn to_datetime_range( - &self, - offset: FixedOffset, - ) -> Result { + pub fn to_datetime_range(&self) -> Result { match self { - PrimitiveValue::Str(s) => { - super::range::parse_datetime_range(s.trim_end().as_bytes(), offset) - .context(ParseDateTimeRangeSnafu) - .map_err(|err| ConvertValueError { - requested: "DateTimeRange", - original: self.value_type(), - cause: Some(err), - }) - } + PrimitiveValue::Str(s) => super::range::parse_datetime_range(s.trim_end().as_bytes()) + .context(ParseDateTimeRangeSnafu) + .map_err(|err| ConvertValueError { + requested: "DateTimeRange", + original: self.value_type(), + cause: Some(err), + }), PrimitiveValue::Strs(s) => super::range::parse_datetime_range( s.first().map(|s| s.trim_end().as_bytes()).unwrap_or(&[]), - offset, ) .context(ParseDateTimeRangeSnafu) .map_err(|err| ConvertValueError { @@ -3230,7 +3215,7 @@ impl PrimitiveValue { cause: Some(err), }), PrimitiveValue::U8(bytes) => { - super::range::parse_datetime_range(trim_last_whitespace(bytes), offset) + super::range::parse_datetime_range(trim_last_whitespace(bytes)) .context(ParseDateTimeRangeSnafu) .map_err(|err| ConvertValueError { requested: "DateTimeRange", @@ -4098,7 +4083,7 @@ impl PrimitiveValue { /// Shorten this value by removing trailing elements /// to fit the given limit. - /// + /// /// Elements are counted by the number of individual value items /// (note that bytes in a [`PrimitiveValue::U8`] /// are treated as individual items). @@ -4121,8 +4106,7 @@ impl PrimitiveValue { /// ``` pub fn truncate(&mut self, limit: usize) { match self { - PrimitiveValue::Empty | - PrimitiveValue::Str(_) => { /* no-op */ }, + PrimitiveValue::Empty | PrimitiveValue::Str(_) => { /* no-op */ } PrimitiveValue::Strs(l) => l.truncate(limit), PrimitiveValue::Tags(l) => l.truncate(limit), PrimitiveValue::U8(l) => l.truncate(limit), @@ -4927,57 +4911,72 @@ mod tests { fn primitive_value_to_dicom_datetime() { let offset = FixedOffset::east_opt(1).unwrap(); - // try from chrono::DateTime + // try from chrono::DateTime assert_eq!( PrimitiveValue::from( - DicomDateTime::from_date_and_time( + DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_ymd(2012, 12, 21).unwrap(), DicomTime::from_hms_micro(11, 9, 26, 000123).unwrap(), offset ) .unwrap() ) - .to_datetime(offset) + .to_datetime() .unwrap(), - DicomDateTime::from_date_and_time( + DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_ymd(2012, 12, 21).unwrap(), DicomTime::from_hms_micro(11, 9, 26, 000123).unwrap(), offset ) .unwrap() ); + // try from chrono::NaiveDateTime + assert_eq!( + PrimitiveValue::from( + DicomDateTime::from_date_and_time( + DicomDate::from_ymd(2012, 12, 21).unwrap(), + DicomTime::from_hms_micro(11, 9, 26, 000123).unwrap() + ) + .unwrap() + ) + .to_datetime() + .unwrap(), + DicomDateTime::from_date_and_time( + DicomDate::from_ymd(2012, 12, 21).unwrap(), + DicomTime::from_hms_micro(11, 9, 26, 000123).unwrap() + ) + .unwrap() + ); // from text (Str) - minimum allowed is a YYYY assert_eq!( - dicom_value!(Str, "2012").to_datetime(offset).unwrap(), - DicomDateTime::from_date(DicomDate::from_y(2012).unwrap(), offset) + dicom_value!(Str, "2012").to_datetime().unwrap(), + DicomDateTime::from_date(DicomDate::from_y(2012).unwrap()) ); // from text with fraction of a second + padding assert_eq!( PrimitiveValue::from("20121221110926.38 ") - .to_datetime(offset) + .to_datetime() .unwrap(), DicomDateTime::from_date_and_time( DicomDate::from_ymd(2012, 12, 21).unwrap(), - DicomTime::from_hmsf(11, 9, 26, 38, 2).unwrap(), - offset + DicomTime::from_hmsf(11, 9, 26, 38, 2).unwrap() ) .unwrap() ); // from text (Strs) with fraction of a second + padding assert_eq!( dicom_value!(Strs, ["20121221110926.38 "]) - .to_datetime(offset) + .to_datetime() .unwrap(), DicomDateTime::from_date_and_time( DicomDate::from_ymd(2012, 12, 21).unwrap(), - DicomTime::from_hmsf(11, 9, 26, 38, 2).unwrap(), - offset + DicomTime::from_hmsf(11, 9, 26, 38, 2).unwrap() ) .unwrap() ); // not a dicom_datetime assert!(matches!( - PrimitiveValue::from("Smith^John").to_datetime(offset), + PrimitiveValue::from("Smith^John").to_datetime(), Err(ConvertValueError { requested: "DicomDateTime", original: ValueType::Str, @@ -4988,28 +4987,26 @@ mod tests { #[test] fn primitive_value_to_multi_dicom_datetime() { - let offset = FixedOffset::east_opt(1).unwrap(); // from text (Strs) assert_eq!( dicom_value!( Strs, ["20121221110926.38 ", "1992", "19901010-0500", "1990+0501"] ) - .to_multi_datetime(offset) + .to_multi_datetime() .unwrap(), vec!( DicomDateTime::from_date_and_time( DicomDate::from_ymd(2012, 12, 21).unwrap(), - DicomTime::from_hmsf(11, 9, 26, 38, 2).unwrap(), - offset + DicomTime::from_hmsf(11, 9, 26, 38, 2).unwrap() ) .unwrap(), - DicomDateTime::from_date(DicomDate::from_y(1992).unwrap(), offset), - DicomDateTime::from_date( + DicomDateTime::from_date(DicomDate::from_y(1992).unwrap()), + DicomDateTime::from_date_with_time_zone( DicomDate::from_ymd(1990, 10, 10).unwrap(), FixedOffset::west_opt(5 * 3600).unwrap() ), - DicomDateTime::from_date( + DicomDateTime::from_date_with_time_zone( DicomDate::from_y(1990).unwrap(), FixedOffset::east_opt(5 * 3600 + 60).unwrap() ) @@ -5046,26 +5043,30 @@ mod tests { assert_eq!( dicom_value!(Str, "202002-20210228153012.123") - .to_datetime_range(offset) + .to_datetime_range() .unwrap(), DateTimeRange::from_start_to_end( - offset.with_ymd_and_hms(2020, 2, 1, 0, 0, 0).unwrap(), - offset - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2021, 2, 28).unwrap(), - NaiveTime::from_hms_micro_opt(15, 30, 12, 123_999).unwrap() - )) - .unwrap() + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2020, 2, 1).unwrap(), + NaiveTime::from_hms_opt(0, 0, 0).unwrap() + ), + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2021, 2, 28).unwrap(), + NaiveTime::from_hms_micro_opt(15, 30, 12, 123_999).unwrap() + ) ) .unwrap() ); // East UTC offset gets parsed assert_eq!( PrimitiveValue::from(&b"2020-2030+0800"[..]) - .to_datetime_range(offset) + .to_datetime_range() .unwrap(), - DateTimeRange::from_start_to_end( - offset.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(), + DateTimeRange::from_start_to_end_with_time_zone( + FixedOffset::east_opt(0) // this offset is missing, so generated + .unwrap() + .with_ymd_and_hms(2020, 1, 1, 0, 0, 0) + .unwrap(), FixedOffset::east_opt(8 * 3600) .unwrap() .from_local_datetime(&NaiveDateTime::new( @@ -5138,7 +5139,7 @@ mod tests { // b"20121221093001+0100 " let offset = FixedOffset::east_opt(1 * 3600).unwrap(); let val = PrimitiveValue::from( - DicomDateTime::from_date_and_time( + DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_ymd(2012, 12, 21).unwrap(), DicomTime::from_hms(9, 30, 1).unwrap(), offset, @@ -5146,6 +5147,17 @@ mod tests { .unwrap(), ); assert_eq!(val.calculate_byte_len(), 20); + + // single date-time without time zone, no second fragment + // b"20121221093001 " + let val = PrimitiveValue::from( + DicomDateTime::from_date_and_time( + DicomDate::from_ymd(2012, 12, 21).unwrap(), + DicomTime::from_hms(9, 30, 1).unwrap(), + ) + .unwrap(), + ); + assert_eq!(val.calculate_byte_len(), 14); } #[test] diff --git a/core/src/value/range.rs b/core/src/value/range.rs index 9ec463fe4..5c12dfa1f 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -1,7 +1,9 @@ //! Handling of date, time, date-time ranges. Needed for range matching. //! Parsing into ranges happens via partial precision structures (DicomDate, DicomTime, //! DicomDatime) so ranges can handle null components in date, time, date-time values. -use chrono::{DateTime, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone}; +use chrono::{ + offset, DateTime, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, +}; use snafu::{Backtrace, OptionExt, ResultExt, Snafu}; use crate::value::deserialize::{ @@ -29,8 +31,12 @@ pub enum Error { NoRangeSeparator { backtrace: Backtrace }, #[snafu(display("Date-time range can contain 1-3 '-' characters, {} were found", value))] SeparatorCount { value: usize, backtrace: Backtrace }, - #[snafu(display("Invalid date-time"))] - InvalidDateTime { 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" ))] @@ -157,7 +163,7 @@ pub trait AsRange: Precision { impl AsRange for DicomDate { type Item = NaiveDate; type Range = DateRange; - fn earliest(&self) -> Result { + fn earliest(&self) -> Result { let (y, m, d) = { ( *self.year() as i32, @@ -168,7 +174,7 @@ impl AsRange for DicomDate { NaiveDate::from_ymd_opt(y, m, d).context(InvalidDateSnafu { y, m, d }) } - fn latest(&self) -> Result { + fn latest(&self) -> Result { let (y, m, d) = ( self.year(), self.month().unwrap_or(&12), @@ -213,7 +219,7 @@ impl AsRange for DicomDate { }) } - fn range(&self) -> Result { + fn range(&self) -> Result { let start = self.earliest()?; let end = self.latest()?; DateRange::from_start_to_end(start, end) @@ -223,7 +229,7 @@ impl AsRange for DicomDate { impl AsRange for DicomTime { type Item = NaiveTime; type Range = TimeRange; - fn earliest(&self) -> Result { + fn earliest(&self) -> Result { let (h, m, s, f) = ( self.hour(), self.minute().unwrap_or(&0), @@ -243,7 +249,7 @@ impl AsRange for DicomTime { }, ) } - fn latest(&self) -> Result { + fn latest(&self) -> Result { let (h, m, s, f) = ( self.hour(), self.minute().unwrap_or(&59), @@ -264,7 +270,7 @@ impl AsRange for DicomTime { }, ) } - fn range(&self) -> Result { + fn range(&self) -> Result { let start = self.earliest()?; let end = self.latest()?; TimeRange::from_start_to_end(start, end) @@ -272,9 +278,9 @@ impl AsRange for DicomTime { } impl AsRange for DicomDateTime { - type Item = NaiveDateTime; + type Item = PreciseDateTimeResult; type Range = DateTimeRange; - fn earliest(&self) -> Result> { + fn earliest(&self) -> Result { let date = self.date().earliest()?; let time = match self.time() { Some(time) => time.earliest()?, @@ -285,13 +291,21 @@ impl AsRange for DicomDateTime { })?, }; - self.offset() - .from_local_datetime(&NaiveDateTime::new(date, time)) - .single() - .context(InvalidDateTimeSnafu) + match self.time_zone() { + Some(offset) => Ok(PreciseDateTimeResult::WithTimeZone( + offset + .from_local_datetime(&NaiveDateTime::new(date, time)) + .single() + .context(InvalidDateTimeSnafu { + naive: NaiveDateTime::new(date, time), + offset: *offset, + })?, + )), + None => Ok(PreciseDateTimeResult::Naive(NaiveDateTime::new(date, time))), + } } - fn latest(&self) -> Result> { + fn latest(&self) -> Result { let date = self.date().latest()?; let time = match self.time() { Some(time) => time.latest()?, @@ -304,15 +318,39 @@ impl AsRange for DicomDateTime { }, )?, }; - self.offset() - .from_local_datetime(&NaiveDateTime::new(date, time)) - .single() - .context(InvalidDateTimeSnafu) + + match self.time_zone() { + Some(offset) => Ok(PreciseDateTimeResult::WithTimeZone( + offset + .from_local_datetime(&NaiveDateTime::new(date, time)) + .single() + .context(InvalidDateTimeSnafu { + naive: NaiveDateTime::new(date, time), + offset: *offset, + })?, + )), + None => Ok(PreciseDateTimeResult::Naive(NaiveDateTime::new(date, time))), + } } - fn range(&self) -> Result { + fn range(&self) -> Result { let start = self.earliest()?; let end = self.latest()?; - DateTimeRange::from_start_to_end(start, end) + + if start.has_time_zone() { + let s = start.into_datetime_with_time_zone()?; + let e = end.into_datetime_with_time_zone()?; + Ok(DateTimeRange::WithTimeZone { + start: Some(s), + end: Some(e), + }) + } else { + let s = start.into_datetime()?; + let e = end.into_datetime()?; + Ok(DateTimeRange::Naive { + start: Some(s), + end: Some(e), + }) + } } } @@ -338,47 +376,37 @@ impl DicomTime { } impl DicomDateTime { - /// Retrieves a `chrono::DateTime` by converting the internal time-zone naive date-time representation - /// to a time-zone aware representation. + /// Retrieves a `chrono::DateTime`. /// It the value does not store a time-zone or the date-time value is not precise or the conversion leads to ambiguous results, /// it fails. /// To inspect the possibly ambiguous results of this conversion, see `to_chrono_local_result` - pub fn to_chrono_datetime(self) -> Result> { - if let Some(offset) = self.time_zone() { - match offset.from_local_datetime(&self.exact()?) { - LocalResult::Single(date_time) => Ok(date_time), - LocalResult::Ambiguous(t1, t2) => DateTimeAmbiguousSnafu { t1, t2 }.fail(), - LocalResult::None => DateTimeInvalidSnafu.fail(), - } - } else { - NoTimeZoneSnafu.fail() - } + pub fn to_datetime_with_time_zone(self) -> Result> { + self.exact()?.into_datetime_with_time_zone() + } + + /// Retrieves a `chrono::NaiveDateTime`. If the internal date-time value is not precise or + /// it is time-zone aware, this method will fail. + pub fn to_naive_datetime(&self) -> Result { + self.exact()?.into_datetime() } /// Retrieves a `chrono::LocalResult` by converting the internal time-zone naive date-time representation /// to a time-zone aware representation. /// It the value does not store a time-zone or the date-time value is not precise, it fails. pub fn to_chrono_local_result(self) -> Result>> { - if let Some(offset) = self.time_zone() { - Ok(offset.from_local_datetime(&self.exact()?)) - } else { - NoTimeZoneSnafu.fail() + if !self.is_precise() { + return ImpreciseValueSnafu.fail(); } - } - /// Retrieves a `chrono::NaiveDateTime`. If the internal date-time value is not precise or - /// it is time-zone aware, this method will fail. - pub fn to_naive_datetime(&self) -> Result{ - if self.time_zone().is_some(){ + if let Some(offset) = self.time_zone() { + let date = self.date().earliest()?; + let time = self.time().context(ImpreciseValueSnafu)?.earliest()?; + let naive_date_time = NaiveDateTime::new(date, time); + Ok(offset.from_local_datetime(&naive_date_time)) + } else { + NoTimeZoneSnafu.fail() } - self.exact() - } - - /// Retrieves a `chrono::NaiveDateTime` it the value is precise. This method will work even if the - /// date-time value is time-zone aware and thus expects that you know what you are doing. - pub fn to_naive_datetime_ignore_timezone(&self) -> Result{ - self.exact() } } @@ -416,8 +444,10 @@ pub struct TimeRange { start: Option, end: Option, } -/// Represents a date-time range as two [`Option>`] values. +/// Represents a date-time range, that can either be time-zone naive or time-zone aware. It is stored as two [`Option>`] or +/// two [`Option>`] values. /// [None] means no upper or no lower bound for range is present. +/// Please note, that this structure is guaranteed never to contain a combination of one time-zone aware and one time-zone naive value. /// # Example /// ``` /// # use std::error::Error; @@ -444,9 +474,65 @@ pub struct TimeRange { /// # } /// ``` #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] -pub struct DateTimeRange { - start: Option>, - end: Option>, +pub enum DateTimeRange { + Naive { + start: Option, + end: Option, + }, + WithTimeZone { + start: Option>, + end: Option>, + }, +} + +/// A precise date-time value, that can either be time-zone aware or time-zone unaware. +/// +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd)] +pub enum PreciseDateTimeResult { + Naive(NaiveDateTime), + WithTimeZone(DateTime), +} + +impl PreciseDateTimeResult { + /// Retrieves a reference to a `chrono::DateTime` if the result is time-zone aware. + pub fn as_datetime_with_time_zone(&self) -> Option<&DateTime> { + match self { + PreciseDateTimeResult::Naive(..) => None, + PreciseDateTimeResult::WithTimeZone(value) => Some(value), + } + } + + /// Retrieves a reference to a `chrono::NaiveDateTime` if the result is time-zone naive. + pub fn as_datetime(&self) -> Option<&NaiveDateTime> { + match self { + PreciseDateTimeResult::Naive(value) => Some(value), + PreciseDateTimeResult::WithTimeZone(..) => None, + } + } + + /// Moves out a `chrono::DateTime` if the result is time-zone aware, otherwise it fails. + pub fn into_datetime_with_time_zone(self) -> Result> { + match self { + PreciseDateTimeResult::Naive(..) => NoTimeZoneSnafu.fail(), + PreciseDateTimeResult::WithTimeZone(value) => Ok(value), + } + } + + /// Moves out a `chrono::NaiveDateTime` if the result is time-zone naive, otherwise it fails. + pub fn into_datetime(self) -> Result { + match self { + PreciseDateTimeResult::Naive(value) => Ok(value), + PreciseDateTimeResult::WithTimeZone(..) => DateTimeTzAwareSnafu.fail(), + } + } + + /// Returns true if result is time-zone aware. + pub fn has_time_zone(&self) -> bool { + match self { + PreciseDateTimeResult::Naive(..) => false, + PreciseDateTimeResult::WithTimeZone(..) => true, + } + } } impl DateRange { @@ -542,9 +628,9 @@ impl TimeRange { } impl DateTimeRange { - /// Constructs a new `DateTimeRange` from two `chrono::DateTime` values + /// Constructs a new `DateTimeRange` from two `chrono::DateTime` values /// monotonically ordered in time. - pub fn from_start_to_end( + pub fn from_start_to_end_with_time_zone( start: DateTime, end: DateTime, ) -> Result { @@ -555,47 +641,89 @@ impl DateTimeRange { } .fail() } else { - Ok(DateTimeRange { + Ok(DateTimeRange::WithTimeZone { start: Some(start), end: Some(end), }) } } - /// Constructs a new `DateTimeRange` beginning with a `chrono::DateTime` value + /// Constructs a new `DateTimeRange` from two `chrono::NaiveDateTime` values + /// monotonically ordered in time. + pub fn from_start_to_end(start: NaiveDateTime, end: NaiveDateTime) -> Result { + if start > end { + RangeInversionSnafu { + start: start.to_string(), + end: end.to_string(), + } + .fail() + } else { + Ok(DateTimeRange::Naive { + start: Some(start), + end: Some(end), + }) + } + } + + /// Constructs a new `DateTimeRange` beginning with a `chrono::DateTime` value + /// and no upper limit. + pub fn from_start_with_time_zone(start: DateTime) -> DateTimeRange { + DateTimeRange::WithTimeZone { + start: Some(start), + end: None, + } + } + + /// Constructs a new `DateTimeRange` beginning with a `chrono::NaiveDateTime` value /// and no upper limit. - pub fn from_start(start: DateTime) -> DateTimeRange { - DateTimeRange { + pub fn from_start(start: NaiveDateTime) -> DateTimeRange { + DateTimeRange::Naive { start: Some(start), end: None, } } - /// Constructs a new `DateTimeRange` with no lower limit, ending with a `chrono::DateTime` value. - pub fn from_end(end: DateTime) -> DateTimeRange { - DateTimeRange { + /// Constructs a new `DateTimeRange` with no lower limit, ending with a `chrono::DateTime` value. + pub fn from_end_with_time_zone(end: DateTime) -> DateTimeRange { + DateTimeRange::WithTimeZone { start: None, end: Some(end), } } - /// Returns a reference to the lower bound of the range. - pub fn start(&self) -> Option<&DateTime> { - self.start.as_ref() + /// Constructs a new `DateTimeRange` with no lower limit, ending with a `chrono::NaiveDateTime` value. + pub fn from_end(end: NaiveDateTime) -> DateTimeRange { + DateTimeRange::Naive { + start: None, + end: Some(end), + } } - /// Returns a reference to the upper bound of the range. - pub fn end(&self) -> Option<&DateTime> { - self.end.as_ref() + /// Returns the lower bound of the range. + pub fn start(&self) -> Option { + match self { + DateTimeRange::Naive { start, .. } => start.map(|dt| PreciseDateTimeResult::Naive(dt)), + DateTimeRange::WithTimeZone { start, .. } => { + start.map(|dt| PreciseDateTimeResult::WithTimeZone(dt)) + } + } + } + + /// Returns the upper bound of the range. + pub fn end(&self) -> Option { + match self { + DateTimeRange::Naive { start, end } => end.map(|dt| PreciseDateTimeResult::Naive(dt)), + DateTimeRange::WithTimeZone { start, end } => { + end.map(|dt| PreciseDateTimeResult::WithTimeZone(dt)) + } + } } /// For combined datetime range matching, /// this method constructs a `DateTimeRange` from a `DateRange` and a `TimeRange`. - pub fn from_date_and_time_range( - dr: DateRange, - tr: TimeRange, - offset: FixedOffset, - ) -> Result { + /// As 'DateRange' and 'TimeRange' are always time-zone unaware, the resulting DateTimeRange + /// will always be time-zone unaware. + pub fn from_date_and_time_range(dr: DateRange, tr: TimeRange) -> Result { let start_date = dr.start(); let end_date = dr.end(); @@ -620,33 +748,27 @@ impl DateTimeRange { match start_date { Some(sd) => match end_date { Some(ed) => Ok(DateTimeRange::from_start_to_end( - offset - .from_local_datetime(&NaiveDateTime::new(*sd, start_time)) - .single() - .context(InvalidDateTimeSnafu)?, - offset - .from_local_datetime(&NaiveDateTime::new(*ed, end_time)) - .single() - .context(InvalidDateTimeSnafu)?, + NaiveDateTime::new(*sd, start_time), + NaiveDateTime::new(*ed, end_time), )?), - None => Ok(DateTimeRange::from_start( - offset - .from_local_datetime(&NaiveDateTime::new(*sd, start_time)) - .single() - .context(InvalidDateTimeSnafu)?, - )), + None => Ok(DateTimeRange::from_start(NaiveDateTime::new( + *sd, start_time, + ))), }, None => match end_date { - Some(ed) => Ok(DateTimeRange::from_end( - offset - .from_local_datetime(&NaiveDateTime::new(*ed, end_time)) - .single() - .context(InvalidDateTimeSnafu)?, - )), + 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 has_time_zone(&self) -> bool { + self.time_zone().is_some() + } + + pub fn time_zone(&self) -> Option<&FixedOffset> { + self.time_zone() + } } /** @@ -721,14 +843,17 @@ pub fn parse_time_range(buf: &[u8]) -> Result { /// Looks for a range separator '-'. /// Returns a `DateTimeRange`. +/// If the parser encounters two date-time values, where one is time-zone aware and the other is not, +/// it will automatically convert the time-zone naive value to a time-zone aware value with zero east offset. +/// (same as appending '+0000' to the text representation). /// Users are advised, that for very specific inputs, inconsistent behavior can occur. /// This behavior can only be produced when all of the following is true: -/// - two very short date-times in the form of YYYY are presented -/// - both YYYY values can be exchanged for a valid west UTC offset, meaning year <= 1200 -/// - only one west UTC offset is presented. -/// In such cases, two '-' characters are present and the parser will favor the first one, +/// - two very short date-times in the form of YYYY are presented (YYYY-YYYY) +/// - both YYYY values can be exchanged for a valid west UTC offset, meaning year <= 1200 e.g. (1000-1100) +/// - only one west UTC offset is presented. e.g. (1000-1100-0100) +/// In such cases, two '-' characters are present and the parser will favor the first one as a range separator, /// if it produces a valid `DateTimeRange`. Otherwise, it tries the second one. -pub fn parse_datetime_range(buf: &[u8], dt_utc_offset: FixedOffset) -> Result { +pub fn parse_datetime_range(buf: &[u8]) -> Result { // minimum length of one valid DicomDateTime (YYYY) and one '-' separator if buf.len() < 5 { return UnexpectedEndOfElementSnafu.fail(); @@ -737,19 +862,24 @@ pub fn parse_datetime_range(buf: &[u8], dt_utc_offset: FixedOffset) -> Result Ok(DateTimeRange::from_end(end)), + PreciseDateTimeResult::WithTimeZone(end_tz) => { + Ok(DateTimeRange::from_end_with_time_zone(end_tz)) + } + } } else if buf[buf.len() - 1] == b'-' { // ends with separator, range is Some-None let buf = &buf[0..(buf.len() - 1)]; - Ok(DateTimeRange::from_start( - parse_datetime_partial(buf, dt_utc_offset) - .context(ParseSnafu)? - .earliest()?, - )) + match parse_datetime_partial(buf) + .context(ParseSnafu)? + .earliest()? + { + PreciseDateTimeResult::Naive(start) => Ok(DateTimeRange::from_start(start)), + PreciseDateTimeResult::WithTimeZone(start_tz) => { + Ok(DateTimeRange::from_start_with_time_zone(start_tz)) + } + } } else { // range must be Some-Some, now, count number of dashes and get their indexes let dashes: Vec = buf @@ -767,14 +897,53 @@ pub fn parse_datetime_range(buf: &[u8], dt_utc_offset: FixedOffset) -> Result { //create a result here, to check for range inversion - let dtr = DateTimeRange::from_start_to_end(s.earliest()?, e.latest()?); + let dtr = match (s.earliest()?, e.latest()?) { + ( + PreciseDateTimeResult::Naive(start), + PreciseDateTimeResult::Naive(end), + ) => DateTimeRange::from_start_to_end(start, end), + ( + PreciseDateTimeResult::WithTimeZone(start), + PreciseDateTimeResult::WithTimeZone(end), + ) => DateTimeRange::from_start_to_end_with_time_zone(start, end), + ( + PreciseDateTimeResult::Naive(start), + PreciseDateTimeResult::WithTimeZone(end), + ) => { + let offset = FixedOffset::east_opt(0).unwrap(); + DateTimeRange::from_start_to_end_with_time_zone( + offset.from_local_datetime(&start).single().context( + InvalidDateTimeSnafu { + naive: start, + offset: offset, + }, + )?, + end, + ) + } + ( + PreciseDateTimeResult::WithTimeZone(start), + PreciseDateTimeResult::Naive(end), + ) => { + let offset = FixedOffset::east_opt(0).unwrap(); + DateTimeRange::from_start_to_end_with_time_zone( + start, + offset.from_local_datetime(&end).single().context( + InvalidDateTimeSnafu { + naive: end, + offset: offset, + }, + )?, + ) + } + }; match dtr { Ok(val) => return Ok(val), Err(_) => dashes[1], @@ -789,14 +958,47 @@ pub fn parse_datetime_range(buf: &[u8], dt_utc_offset: FixedOffset) -> Result { + DateTimeRange::from_start_to_end(start, end) + } + ( + PreciseDateTimeResult::WithTimeZone(start), + PreciseDateTimeResult::WithTimeZone(end), + ) => DateTimeRange::from_start_to_end_with_time_zone(start, end), + (PreciseDateTimeResult::Naive(start), PreciseDateTimeResult::WithTimeZone(end)) => { + let offset = FixedOffset::east_opt(0).unwrap(); + DateTimeRange::from_start_to_end_with_time_zone( + offset + .from_local_datetime(&start) + .single() + .context(InvalidDateTimeSnafu { + naive: start, + offset: offset, + })?, + end, + ) + } + (PreciseDateTimeResult::WithTimeZone(start), PreciseDateTimeResult::Naive(end)) => { + let offset = FixedOffset::east_opt(0).unwrap(); + DateTimeRange::from_start_to_end_with_time_zone( + start, + offset + .from_local_datetime(&end) + .single() + .context(InvalidDateTimeSnafu { + naive: end, + offset: offset, + })?, + ) + } + } } } @@ -881,11 +1083,11 @@ mod tests { } #[test] - fn test_datetime_range() { + fn test_datetime_range_with_time_zone() { let offset = FixedOffset::west_opt(3600).unwrap(); assert_eq!( - DateTimeRange::from_start( + DateTimeRange::from_start_with_time_zone( offset .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), @@ -894,17 +1096,18 @@ mod tests { .unwrap() ) .start(), - Some( - &offset + Some(PreciseDateTimeResult::WithTimeZone( + 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( + DateTimeRange::from_end_with_time_zone( offset .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), @@ -913,17 +1116,18 @@ mod tests { .unwrap() ) .end(), - Some( - &offset + Some(PreciseDateTimeResult::WithTimeZone( + 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( + DateTimeRange::from_start_to_end_with_time_zone( offset .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), @@ -939,17 +1143,17 @@ mod tests { ) .unwrap() .start(), - Some( - &offset + Some(PreciseDateTimeResult::WithTimeZone( + 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( + DateTimeRange::from_start_to_end_with_time_zone( offset .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), @@ -965,17 +1169,17 @@ mod tests { ) .unwrap() .end(), - Some( - &offset + Some(PreciseDateTimeResult::WithTimeZone( + 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( + DateTimeRange::from_start_to_end_with_time_zone( offset .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), @@ -997,6 +1201,102 @@ mod tests { )); } + #[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(PreciseDateTimeResult::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(PreciseDateTimeResult::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(PreciseDateTimeResult::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(PreciseDateTimeResult::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!( @@ -1108,109 +1408,95 @@ mod tests { #[test] fn test_parse_datetime_range() { - let offset = FixedOffset::west_opt(3600).unwrap(); + assert_eq!( - parse_datetime_range(b"-20200229153420.123456", offset).ok(), - Some(DateTimeRange { + parse_datetime_range(b"-20200229153420.123456").ok(), + Some(DateTimeRange::Naive { start: None, end: Some( - offset - .from_local_datetime(&NaiveDateTime::new( + NaiveDateTime::new( NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(), NaiveTime::from_hms_micro_opt(15, 34, 20, 123_456).unwrap() - )) - .unwrap() + ) ) }) ); assert_eq!( - parse_datetime_range(b"-20200229153420.123", offset).ok(), - Some(DateTimeRange { + parse_datetime_range(b"-20200229153420.123").ok(), + Some(DateTimeRange::Naive { start: None, end: Some( - offset - .from_local_datetime(&NaiveDateTime::new( + NaiveDateTime::new( NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(), NaiveTime::from_hms_micro_opt(15, 34, 20, 123_999).unwrap() - )) - .unwrap() + ) ) }) ); assert_eq!( - parse_datetime_range(b"-20200229153420", offset).ok(), - Some(DateTimeRange { + parse_datetime_range(b"-20200229153420").ok(), + Some(DateTimeRange::Naive { start: None, end: Some( - offset - .from_local_datetime(&NaiveDateTime::new( + NaiveDateTime::new( NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(), NaiveTime::from_hms_micro_opt(15, 34, 20, 999_999).unwrap() - )) - .unwrap() + ) ) }) ); assert_eq!( - parse_datetime_range(b"-2020022915", offset).ok(), - Some(DateTimeRange { + parse_datetime_range(b"-2020022915").ok(), + Some(DateTimeRange::Naive { start: None, end: Some( - offset - .from_local_datetime(&NaiveDateTime::new( + NaiveDateTime::new( NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(), NaiveTime::from_hms_micro_opt(15, 59, 59, 999_999).unwrap() - )) - .unwrap() + ) ) }) ); assert_eq!( - parse_datetime_range(b"-202002", offset).ok(), - Some(DateTimeRange { + parse_datetime_range(b"-202002").ok(), + Some(DateTimeRange::Naive { start: None, end: Some( - offset - .from_local_datetime(&NaiveDateTime::new( + NaiveDateTime::new( NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(), NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() - )) - .unwrap() + ) ) }) ); assert_eq!( - parse_datetime_range(b"0002-", offset).ok(), - Some(DateTimeRange { + parse_datetime_range(b"0002-").ok(), + Some(DateTimeRange::Naive { start: Some( - offset - .from_local_datetime(&NaiveDateTime::new( + NaiveDateTime::new( NaiveDate::from_ymd_opt(2, 1, 1).unwrap(), NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() - )) - .unwrap() + ) ), end: None }) ); assert_eq!( - parse_datetime_range(b"00021231-", offset).ok(), - Some(DateTimeRange { + parse_datetime_range(b"00021231-").ok(), + Some(DateTimeRange::Naive { start: Some( - offset - .from_local_datetime(&NaiveDateTime::new( + NaiveDateTime::new( NaiveDate::from_ymd_opt(2, 12, 31).unwrap(), NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() - )) - .unwrap() + ) ), end: None }) ); // two 'east' UTC offsets get parsed assert_eq!( - parse_datetime_range(b"19900101+0500-1999+1400", offset).ok(), - Some(DateTimeRange { + parse_datetime_range(b"19900101+0500-1999+1400").ok(), + Some(DateTimeRange::WithTimeZone { start: Some( FixedOffset::east_opt(5 * 3600) .unwrap() @@ -1231,10 +1517,10 @@ mod tests { ) }) ); - // two 'west' UTC offsets get parsed + // two 'west' Time zone offsets get parsed assert_eq!( - parse_datetime_range(b"19900101-0500-1999-1200", offset).ok(), - Some(DateTimeRange { + parse_datetime_range(b"19900101-0500-1999-1200").ok(), + Some(DateTimeRange::WithTimeZone { start: Some( FixedOffset::west_opt(5 * 3600) .unwrap() @@ -1255,10 +1541,10 @@ mod tests { ) }) ); - // 'east' and 'west' UTC offsets get parsed + // 'east' and 'west' Time zone offsets get parsed assert_eq!( - parse_datetime_range(b"19900101+1400-1999-1200", offset).ok(), - Some(DateTimeRange { + parse_datetime_range(b"19900101+1400-1999-1200").ok(), + Some(DateTimeRange::WithTimeZone { start: Some( FixedOffset::east_opt(14 * 3600) .unwrap() @@ -1279,10 +1565,11 @@ mod tests { ) }) ); - // one 'west' UTC offsets gets parsed, offset cannot be mistaken for a date-time + // one 'west' Time zone offset gets parsed, offset cannot be mistaken for a date-time + // the missing Time zone offset of the upper bound is generated as a east zero offset. assert_eq!( - parse_datetime_range(b"19900101-1200-1999", offset).unwrap(), - DateTimeRange { + parse_datetime_range(b"19900101-1200-1999").unwrap(), + DateTimeRange::WithTimeZone { start: Some( FixedOffset::west_opt(12 * 3600) .unwrap() @@ -1293,7 +1580,8 @@ mod tests { .unwrap() ), end: Some( - offset + FixedOffset::east_opt(0) + .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() @@ -1302,13 +1590,15 @@ mod tests { ) } ); - // '0500' can either be a valid west UTC offset on left side, or a valid datime on the right side - // Now, the first dash is considered to be a separator. + // '0500' can either be a valid west UTC offset on the lower bound, or a valid date-time on the upper bound + // Now, the first dash is considered to be a range separator, so the lower bound time-zone offset is missing + // and will be generated to be a east zero offset. assert_eq!( - parse_datetime_range(b"0050-0500-1000", offset).unwrap(), - DateTimeRange { + parse_datetime_range(b"0050-0500-1000").unwrap(), + DateTimeRange::WithTimeZone { start: Some( - offset + FixedOffset::east_opt(0) + .unwrap() .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(50, 1, 1).unwrap(), NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() @@ -1328,12 +1618,12 @@ mod tests { ); // sequence with more than 3 dashes '-' is refused. assert!(matches!( - parse_datetime_range(b"0001-00021231-2021-0100-0100", offset), + parse_datetime_range(b"0001-00021231-2021-0100-0100"), Err(Error::SeparatorCount { .. }) )); // any sequence without a dash '-' is refused. assert!(matches!( - parse_datetime_range(b"00021231+0500", offset), + parse_datetime_range(b"00021231+0500"), Err(Error::NoRangeSeparator { .. }) )); } diff --git a/core/src/value/serialize.rs b/core/src/value/serialize.rs index fa99d382d..154417148 100644 --- a/core/src/value/serialize.rs +++ b/core/src/value/serialize.rs @@ -76,26 +76,23 @@ mod test { #[test] fn test_encode_datetime() { let mut data = vec![]; - let offset = FixedOffset::east_opt(0).unwrap(); let bytes = encode_datetime( &mut data, DicomDateTime::from_date_and_time( DicomDate::from_ymd(1985, 12, 31).unwrap(), - DicomTime::from_hms_micro(23, 59, 48, 123_456).unwrap(), - offset, + DicomTime::from_hms_micro(23, 59, 48, 123_456).unwrap() ) .unwrap(), ) .unwrap(); - // even zero offset gets encoded into string value - assert_eq!(from_utf8(&data).unwrap(), "19851231235948.123456+0000"); - assert_eq!(bytes, 26); + assert_eq!(from_utf8(&data).unwrap(), "19851231235948.123456"); + assert_eq!(bytes, 21); let mut data = vec![]; let offset = FixedOffset::east_opt(3600).unwrap(); let bytes = encode_datetime( &mut data, - DicomDateTime::from_date_and_time( + DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_ymd(2018, 12, 24).unwrap(), DicomTime::from_h(4).unwrap(), offset, diff --git a/dump/src/lib.rs b/dump/src/lib.rs index 724d6dbcf..e97a85a11 100644 --- a/dump/src/lib.rs +++ b/dump/src/lib.rs @@ -812,7 +812,7 @@ fn value_summary( } } (Strs(values), VR::DT) => { - match value.to_multi_datetime(dicom_core::chrono::FixedOffset::east_opt(0).unwrap()) { + match value.to_multi_datetime() { Ok(values) => { // print as reformatted date DumpValue::DateTime(format_value_list(values, max_characters, false)) diff --git a/object/src/mem.rs b/object/src/mem.rs index fc0a2d179..1528cf97b 100644 --- a/object/src/mem.rs +++ b/object/src/mem.rs @@ -889,7 +889,7 @@ where AttributeSelectorStep::Nested { tag, item } => { let e = obj .entries - .get(tag) + .get(&tag) .with_context(|| crate::MissingSequenceSnafu { selector: selector.clone(), step_index: i as u32, @@ -1937,7 +1937,7 @@ mod tests { obj.put(instance_number); // add a date time - let dt = DicomDateTime::from_date_and_time( + let dt = DicomDateTime::from_date_and_time_with_time_zone( DicomDate::from_ymd(2022, 11, 22).unwrap(), DicomTime::from_hms(18, 09, 35).unwrap(), FixedOffset::east_opt(3600).unwrap(), diff --git a/parser/src/stateful/decode.rs b/parser/src/stateful/decode.rs index bef304dbf..d37225072 100644 --- a/parser/src/stateful/decode.rs +++ b/parser/src/stateful/decode.rs @@ -230,7 +230,6 @@ pub struct StatefulDecoder { decoder: D, basic: BD, text: TC, - dt_utc_offset: FixedOffset, buffer: Vec, /// the assumed position of the reader source position: u64, @@ -291,7 +290,6 @@ where basic: LittleEndianBasicDecoder, decoder: ExplicitVRLittleEndianDecoder::default(), text: DefaultCharacterSetCodec, - dt_utc_offset: FixedOffset::east_opt(0).unwrap(), buffer: Vec::with_capacity(PARSER_BUFFER_CAPACITY), position: 0, } @@ -322,7 +320,6 @@ where basic, decoder, text, - dt_utc_offset: FixedOffset::east_opt(0).unwrap(), buffer: Vec::with_capacity(PARSER_BUFFER_CAPACITY), position, } @@ -588,7 +585,7 @@ where let vec: Result<_> = buf .split(|b| *b == b'\\') .map(|part| { - parse_datetime_partial(part, self.dt_utc_offset).context(DeserializeValueSnafu { + parse_datetime_partial(part).context(DeserializeValueSnafu { position: self.position, }) }) From fa063a790c953d89cfafec6b2621f6bf9bcefdd8 Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Tue, 6 Feb 2024 19:46:15 +0100 Subject: [PATCH 03/23] - some doctests work --- core/Cargo.toml | 3 ++ core/src/value/mod.rs | 2 +- core/src/value/partial.rs | 25 ++++++------ core/src/value/primitive.rs | 76 +++++++++++++++++++++---------------- core/src/value/range.rs | 34 +++++++++-------- 5 files changed, 80 insertions(+), 60 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index 72a0ffe36..b49dfc331 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -16,3 +16,6 @@ num-traits = "0.2.12" safe-transmute = "0.11.0" smallvec = "1.6.1" snafu = "0.7.3" + +[lib] +doctest = false \ No newline at end of file diff --git a/core/src/value/mod.rs b/core/src/value/mod.rs index 22adc97b9..8d1283185 100644 --- a/core/src/value/mod.rs +++ b/core/src/value/mod.rs @@ -16,7 +16,7 @@ pub mod serialize; pub use self::deserialize::Error as DeserializeError; pub use self::partial::{DicomDate, DicomDateTime, DicomTime}; pub use self::person_name::PersonName; -pub use self::range::{AsRange, DateRange, DateTimeRange, TimeRange}; +pub use self::range::{AsRange, DateRange, DateTimeRange, TimeRange, PreciseDateTimeResult}; pub use self::primitive::{ CastValueError, ConvertValueError, InvalidValueReadError, ModifyValueError, PrimitiveValue, diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 7d3f60654..2ff00af72 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -176,39 +176,42 @@ enum DicomTimeImpl { /// Represents a Dicom DateTime value with a partial precision, /// where some date or time components may be missing. /// -/// `DicomDateTime` is always internally represented by a [DicomDate] -/// and optionally by a [DicomTime] and a timezone [FixedOffset]. +/// `DicomDateTime` is always internally represented by a [DicomDate]. +/// The [DicomTime] and a timezone [FixedOffset] values are optional. /// -/// It implements [AsRange] trait and optionally holds a [FixedOffset] value, from which corresponding -/// [datetime][DateTime] values can be retrieved. +/// It implements [AsRange] trait, which serves to access usable precise values from `DicomDateTime` values +/// with missing components in the form of [PreciseDateTimeResult]. /// # Example /// ``` /// # use std::error::Error; /// # use std::convert::TryFrom; /// use chrono::{DateTime, FixedOffset, TimeZone, NaiveDateTime, NaiveDate, NaiveTime}; -/// use dicom_core::value::{DicomDate, DicomTime, DicomDateTime, AsRange}; +/// use dicom_core::value::{DicomDate, DicomTime, DicomDateTime, AsRange, PreciseDateTimeResult}; /// # fn main() -> Result<(), Box> { /// /// let offset = FixedOffset::east_opt(3600).unwrap(); /// -/// // the least precise date-time value possible is a 'YYYY' -/// let dt = DicomDateTime::from_date( +/// // lets create the least precise date-time value possible 'YYYY' and make it time-zone aware +/// let dt = DicomDateTime::from_date_with_time_zone( /// DicomDate::from_y(2020)?, /// offset /// ); +/// // the earliest possible value is output as a [PreciseDateTimeResult] /// assert_eq!( -/// Some(dt.earliest()?), +/// dt.earliest()?, +/// PreciseDateTimeResult::WithTimeZone( /// offset.from_local_datetime(&NaiveDateTime::new( /// NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(), /// NaiveTime::from_hms_opt(0, 0, 0).unwrap() -/// )).single() +/// )).single().unwrap()) /// ); /// assert_eq!( -/// Some(dt.latest()?), +/// dt.latest()?, +/// PreciseDateTimeResult::WithTimeZone( /// offset.from_local_datetime(&NaiveDateTime::new( /// NaiveDate::from_ymd_opt(2020, 12, 31).unwrap(), /// NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() -/// )).single() +/// )).single().unwrap()) /// ); /// /// let chrono_datetime = offset.from_local_datetime(&NaiveDateTime::new( diff --git a/core/src/value/primitive.rs b/core/src/value/primitive.rs index 2b61cc95c..96d0923bb 100644 --- a/core/src/value/primitive.rs +++ b/core/src/value/primitive.rs @@ -2674,10 +2674,10 @@ impl PrimitiveValue { /// # fn main() -> Result<(), Box> { /// let default_offset = FixedOffset::east(0); /// - /// // full accuracy `DicomDateTime` can be converted + /// // full accuracy `DicomDateTime` with a time-zone can be converted /// assert_eq!( /// PrimitiveValue::from( - /// DicomDateTime::from_date_and_time( + /// DicomDateTime::from_date_and_time_with_time_zone( /// DicomDate::from_ymd(2012, 12, 21)?, /// DicomTime::from_hms_micro(9, 30, 1, 1)?, /// default_offset @@ -2873,49 +2873,54 @@ impl PrimitiveValue { /// ``` /// # use dicom_core::value::{C, PrimitiveValue}; /// # use smallvec::smallvec; - /// # use chrono::{DateTime, FixedOffset, TimeZone}; + /// # use chrono::{DateTime, FixedOffset, TimeZone, NaiveDateTime, NaiveDate, NaiveTime}; /// # use std::error::Error; - /// use dicom_core::value::{DicomDateTime, AsRange, DateTimeRange}; + /// use dicom_core::value::{DicomDateTime, AsRange, DateTimeRange, PreciseDateTimeResult}; /// /// # fn main() -> Result<(), Box> { - /// let default_offset = FixedOffset::east(0); - /// - /// let dt_value = PrimitiveValue::from("20121221093001.1").to_datetime(default_offset)?; + /// + /// // let's parse a date-time text value with 0.1 second precision without a time-zone. + /// let dt_value = PrimitiveValue::from("20121221093001.1").to_datetime()?; /// /// assert_eq!( /// dt_value.earliest()?, - /// FixedOffset::east(0) - /// .ymd(2012, 12, 21) - /// .and_hms_micro(9, 30, 1, 100_000) + /// PreciseDateTimeResult::Naive(NaiveDateTime::new( + /// NaiveDate::from_ymd_opt(2012, 12, 21).unwrap(), + /// NaiveTime::from_hms_micro_opt(9, 30, 1, 100_000).unwrap() + /// )) /// ); /// assert_eq!( /// dt_value.latest()?, - /// FixedOffset::east(0) - /// .ymd(2012, 12, 21) - /// .and_hms_micro(9, 30, 1, 199_999) + /// PreciseDateTimeResult::Naive(NaiveDateTime::new( + /// NaiveDate::from_ymd_opt(2012, 12, 21).unwrap(), + /// NaiveTime::from_hms_micro_opt(9, 30, 1, 199_999).unwrap() + /// )) /// ); /// - /// let dt_value = PrimitiveValue::from("20121221093001.123456").to_datetime(default_offset)?; + /// let default_offset = FixedOffset::east(3600); + /// // let's parse a date-time text value with full precision with a time-zone east +01:00. + /// let dt_value = PrimitiveValue::from("20121221093001.123456+0100").to_datetime()?; /// /// // date-time has all components /// assert_eq!(dt_value.is_precise(), true); /// - /// assert!(dt_value.exact().is_ok()); - /// - /// // .to_chrono_datetime() only works for a precise value /// assert_eq!( - /// dt_value.to_chrono_datetime()?, - /// dt_value.exact()? + /// dt_value.exact()?, + /// PreciseDateTimeResult::WithTimeZone( + /// default_offset + /// .ymd(2012,12,21) + /// .and_hms_micro(9, 30, 1, 123_456) + /// ) /// ); /// /// // ranges are inclusive, for a precise value, two identical values are returned /// assert_eq!( /// dt_value.range()?, - /// DateTimeRange::from_start_to_end( - /// FixedOffset::east(0) + /// DateTimeRange::from_start_to_end_with_time_zone( + /// FixedOffset::east(3600) /// .ymd(2012, 12, 21) /// .and_hms_micro(9, 30, 1, 123_456), - /// FixedOffset::east(0) + /// FixedOffset::east(3600) /// .ymd(2012, 12, 21) /// .and_hms_micro(9, 30, 1, 123_456))? /// @@ -3165,27 +3170,34 @@ impl PrimitiveValue { /// # use dicom_core::value::{C, PrimitiveValue}; /// use chrono::{DateTime, FixedOffset, TimeZone}; /// # use std::error::Error; - /// use dicom_core::value::{DateTimeRange}; + /// use dicom_core::value::{DateTimeRange, PreciseDateTimeResult}; /// /// # fn main() -> Result<(), Box> { /// - /// let offset = FixedOffset::east(3600); - /// - /// let dt_range = PrimitiveValue::from("19920101153020.123+0500-1993").to_datetime_range(offset)?; + /// // let's parse a text representation of a date-time range, where the lower bound is a microsecond + /// // precision value with a time-zone (east +05:00) and the upper bound is a minimum precision value + /// // without a specified time-zone + /// let dt_range = PrimitiveValue::from("19920101153020.123+0500-1993").to_datetime_range()?; /// - /// // default offset override with parsed value + /// // lower bound of range is parsed into a PreciseDateTimeResult::WithTimeZone variant /// assert_eq!( /// dt_range.start(), - /// Some(&FixedOffset::east(5*3600).ymd(1992, 1, 1) - /// .and_hms_micro(15, 30, 20, 123_000) + /// Some(PreciseDateTimeResult::WithTimeZone( + /// FixedOffset::east_opt(5*3600).unwrap().ymd(1992, 1, 1) + /// .and_hms_micro(15, 30, 20, 123_000) + /// ) /// ) /// ); /// - /// // null components default to latest possible + /// // null components of date-time default to latest possible + /// // because lower bound value is time-zone aware, the upper bound will also be parsed + /// // into a time-zone aware value with /// assert_eq!( /// dt_range.end(), - /// Some(&offset.ymd(1993, 12, 31) - /// .and_hms_micro(23, 59, 59, 999_999) + /// Some(PreciseDateTimeResult::WithTimeZone( + /// FixedOffset::east_opt(0).unwrap().ymd(1993, 12, 31) + /// .and_hms_micro(23, 59, 59, 999_999) + /// ) /// ) /// ); /// diff --git a/core/src/value/range.rs b/core/src/value/range.rs index 5c12dfa1f..f48400b2c 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -127,10 +127,10 @@ type Result = std::result::Result; /// # } /// ``` pub trait AsRange: Precision { - type Item: PartialEq + PartialOrd; + type PreciseValue: PartialEq + PartialOrd; type Range; /// Returns a corresponding `chrono` value, if the partial precision structure has full accuracy. - fn exact(&self) -> Result { + fn exact(&self) -> Result { if self.is_precise() { Ok(self.earliest()?) } else { @@ -138,20 +138,22 @@ pub trait AsRange: Precision { } } - /// Returns the earliest possible `chrono` value from a partial precision structure. + /// Returns the earliest possible valid `chrono` value from a partial precision structure. /// Missing components default to 1 (days, months) or 0 (hours, minutes, ...) /// If structure contains invalid combination of `DateComponent`s, it fails. - fn earliest(&self) -> Result; + fn earliest(&self) -> Result; - /// Returns the latest possible `chrono` value from a partial precision structure. + /// Returns the latest possible valid `chrono` value from a partial precision structure. /// If structure contains invalid combination of `DateComponent`s, it fails. - fn latest(&self) -> Result; + fn latest(&self) -> Result; /// Returns a tuple of the earliest and latest possible value from a partial precision structure. fn range(&self) -> Result; - /// Returns `true` if partial precision structure has the maximum possible accuracy. + /// Returns `true` if partial precision structure has the maximum possible accuracy and is + /// a valid date / time. /// For fraction of a second, the full 6 digits are required for the value to be precise. + /// fn is_precise(&self) -> bool { let e = self.earliest(); let l = self.latest(); @@ -161,9 +163,9 @@ pub trait AsRange: Precision { } impl AsRange for DicomDate { - type Item = NaiveDate; + type PreciseValue = NaiveDate; type Range = DateRange; - fn earliest(&self) -> Result { + fn earliest(&self) -> Result { let (y, m, d) = { ( *self.year() as i32, @@ -174,7 +176,7 @@ impl AsRange for DicomDate { NaiveDate::from_ymd_opt(y, m, d).context(InvalidDateSnafu { y, m, d }) } - fn latest(&self) -> Result { + fn latest(&self) -> Result { let (y, m, d) = ( self.year(), self.month().unwrap_or(&12), @@ -227,9 +229,9 @@ impl AsRange for DicomDate { } impl AsRange for DicomTime { - type Item = NaiveTime; + type PreciseValue = NaiveTime; type Range = TimeRange; - fn earliest(&self) -> Result { + fn earliest(&self) -> Result { let (h, m, s, f) = ( self.hour(), self.minute().unwrap_or(&0), @@ -249,7 +251,7 @@ impl AsRange for DicomTime { }, ) } - fn latest(&self) -> Result { + fn latest(&self) -> Result { let (h, m, s, f) = ( self.hour(), self.minute().unwrap_or(&59), @@ -278,9 +280,9 @@ impl AsRange for DicomTime { } impl AsRange for DicomDateTime { - type Item = PreciseDateTimeResult; + type PreciseValue = PreciseDateTimeResult; type Range = DateTimeRange; - fn earliest(&self) -> Result { + fn earliest(&self) -> Result { let date = self.date().earliest()?; let time = match self.time() { Some(time) => time.earliest()?, @@ -305,7 +307,7 @@ impl AsRange for DicomDateTime { } } - fn latest(&self) -> Result { + fn latest(&self) -> Result { let date = self.date().latest()?; let time = match self.time() { Some(time) => time.latest()?, From 8f57b3fe97ad0cbd19e8ce060aa16e91be0c1b94 Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Tue, 6 Feb 2024 23:42:54 +0100 Subject: [PATCH 04/23] - change precision api and it's docs --- core/Cargo.toml | 3 - core/src/value/mod.rs | 2 +- core/src/value/partial.rs | 126 +++++++++++++++++++++++++++++++++++--- core/src/value/range.rs | 17 ++--- 4 files changed, 124 insertions(+), 24 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index b49dfc331..72a0ffe36 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -16,6 +16,3 @@ num-traits = "0.2.12" safe-transmute = "0.11.0" smallvec = "1.6.1" snafu = "0.7.3" - -[lib] -doctest = false \ No newline at end of file diff --git a/core/src/value/mod.rs b/core/src/value/mod.rs index 8d1283185..7fe305c6c 100644 --- a/core/src/value/mod.rs +++ b/core/src/value/mod.rs @@ -14,7 +14,7 @@ pub mod range; pub mod serialize; pub use self::deserialize::Error as DeserializeError; -pub use self::partial::{DicomDate, DicomDateTime, DicomTime}; +pub use self::partial::{DicomDate, DicomDateTime, DicomTime, Precision}; pub use self::person_name::PersonName; pub use self::range::{AsRange, DateRange, DateTimeRange, TimeRange, PreciseDateTimeResult}; diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 2ff00af72..9c1d98532 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -1,6 +1,5 @@ //! Handling of partial precision of Date, Time and DateTime values. -use crate::value::range::AsRange; use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; use snafu::{Backtrace, ResultExt, Snafu}; use std::convert::{TryFrom, TryInto}; @@ -59,13 +58,21 @@ type Result = std::result::Result; /// Represents components of Date, Time and DateTime values. #[derive(Debug, PartialEq, Copy, Clone, Eq, Hash, PartialOrd, Ord)] pub enum DateComponent { + // year precision Year, + // month precision Month, + // day precision Day, + // hour precision Hour, + // minute precision Minute, + // second precision Second, + // millisecond precision Millisecond, + // microsecond (full second fraction) Fraction, UtcWest, UtcEast, @@ -778,13 +785,70 @@ impl fmt::Debug for DicomDateTime { } } -/** - * This trait is implemented by partial precision - * Date, Time and DateTime structures. - * Trait method returns the last fully precise `DateComponent` of the structure. - */ + +/// This trait is implemented by partial precision date, time and date-time structures. +/// This is useful to easily determine if the date / time value is precise without calling more expensive +/// methods first. +/// [Precision::precision()] method will retrieve the last fully precise component of it's stored date / time value. +/// [Precision::is_precise()] method will check if the given value has full precision. If so, it can be +/// converted with [AsRange::exact()] to a `chrono` value. If not, [AsRange::range()] will yield a +/// date / time range. +/// +/// Please note that precision does not equal validity. A precise 'YYYYMMDD' [DicomDate] can still +/// fail to produce a valid [chrono::NaiveDate] +/// +/// # Example +/// +/// ``` +/// # use dicom_core::value::{C, PrimitiveValue}; +/// use chrono::{NaiveDate}; +/// # use std::error::Error; +/// use dicom_core::value::{DateRange, DicomDate, AsRange, Precision}; +/// # fn main() -> Result<(), Box> { +/// +/// let primitive = PrimitiveValue::from("199402"); +/// +/// // the fastest way to get to a useful value, but it fails not only for invalid +/// // dates but for imprecise ones as well. +/// assert!(primitive.to_naive_date().is_err()); +/// +/// // We should take indermediary steps ... +/// +/// // The parser now checks for basic year and month value ranges here. +/// // But, it would not detect invalid dates like 30th of february etc. +/// let dicom_date : DicomDate = primitive.to_date()?; +/// +/// // now we have a valid DicomDate value, let's check if it's precise. +/// if dicom_date.is_precise(){ +/// // no components are missing, we can proceed by calling .exact() +/// // which calls the `chrono` library +/// let precise_date: NaiveDate = dicom_date.exact()?; +/// } +/// else{ +/// // day and / or month are missing, no need to call expensive .exact() method +/// // - it will fail +/// // try to retrieve the date range instead +/// let date_range: DateRange = dicom_date.range()?; +/// +/// // the real conversion to a `chrono` value only happens at this stage +/// if let Some(start) = date_range.start(){ +/// // the range has a given lower date bound +/// } +/// +/// // or try to retrieve the earliest possible value directly from DicomDate +/// let earliest: NaiveDate = dicom_date.earliest()?; +/// +/// } +/// +/// +/// # Ok(()) +/// # } +/// ``` pub trait Precision { + /// will retrieve the last fully precise component of a date / time structure fn precision(&self) -> DateComponent; + /// returns true if value has all possible components + fn is_precise(&self) -> bool; } impl Precision for DicomDate { @@ -795,6 +859,12 @@ impl Precision for DicomDate { DicomDate(DicomDateImpl::Day(..)) => DateComponent::Day, } } + fn is_precise(&self) -> bool { + match self{ + DicomDate(DicomDateImpl::Day(..)) => true, + _ => false + } + } } impl Precision for DicomTime { @@ -806,6 +876,12 @@ impl Precision for DicomTime { DicomTime(DicomTimeImpl::Fraction(..)) => DateComponent::Fraction, } } + fn is_precise(&self) -> bool { + match self.fraction_and_precision() { + Some((_,fraction_precision)) if fraction_precision == &6 => true, + _ => false + } + } } impl Precision for DicomDateTime { @@ -815,6 +891,12 @@ impl Precision for DicomDateTime { None => self.date.precision(), } } + fn is_precise(&self) -> bool { + match self.time(){ + Some(time) => time.is_precise(), + None => false + } + } } impl DicomDate { @@ -885,7 +967,7 @@ impl DicomDateTime { #[cfg(test)] mod tests { - use crate::value::range::PreciseDateTimeResult; + use crate::value::range::{AsRange,PreciseDateTimeResult}; use super::*; use chrono::{NaiveDateTime, TimeZone}; @@ -896,6 +978,11 @@ mod tests { DicomDate::from_ymd(1944, 2, 29).unwrap(), DicomDate(DicomDateImpl::Day(1944, 2, 29)) ); + + // cheap precision check, but date is invalid + assert!( + DicomDate::from_ymd(1945, 2, 29).unwrap().is_precise() + ); assert_eq!( DicomDate::from_ym(1944, 2).unwrap(), DicomDate(DicomDateImpl::Month(1944, 2)) @@ -975,6 +1062,13 @@ mod tests { DicomTime::from_h(1).unwrap(), DicomTime(DicomTimeImpl::Hour(1)) ); + // cheap precision checks + assert!( + DicomTime::from_hms_micro(9, 1, 1, 123456).unwrap().is_precise() + ); + assert!( + !DicomTime::from_hms_milli(9, 1, 1, 123).unwrap().is_precise() + ); assert_eq!( DicomTime::from_hms_milli(9, 1, 1, 123) @@ -1300,5 +1394,23 @@ mod tests { .exact(), Err(crate::value::range::Error::ImpreciseValue { .. }) )); + + // simple precision checks + assert!( + DicomDateTime::from_date_and_time( + DicomDate::from_ymd(2000, 1, 1).unwrap(), + DicomTime::from_hms_milli(23, 59, 59, 10).unwrap() + ).unwrap() + .is_precise() == false + ); + + assert!( + DicomDateTime::from_date_and_time( + DicomDate::from_ymd(2000, 1, 1).unwrap(), + DicomTime::from_hms_micro(23, 59, 59, 654_321).unwrap() + ).unwrap() + .is_precise() + ); + } } diff --git a/core/src/value/range.rs b/core/src/value/range.rs index f48400b2c..bd5423a07 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -92,6 +92,7 @@ type Result = std::result::Result; /// This trait is implemented by date / time structures with partial precision. /// If the date / time structure is not precise, it is up to the user to call one of these /// methods to retrieve a suitable [`chrono`] value. +/// /// /// # Examples /// @@ -150,16 +151,6 @@ pub trait AsRange: Precision { /// Returns a tuple of the earliest and latest possible value from a partial precision structure. fn range(&self) -> Result; - /// Returns `true` if partial precision structure has the maximum possible accuracy and is - /// a valid date / time. - /// For fraction of a second, the full 6 digits are required for the value to be precise. - /// - fn is_precise(&self) -> bool { - let e = self.earliest(); - let l = self.latest(); - - e.is_ok() && l.is_ok() && e.ok() == l.ok() - } } impl AsRange for DicomDate { @@ -764,13 +755,13 @@ impl DateTimeRange { } } - pub fn has_time_zone(&self) -> bool { + /*pub fn has_time_zone(&self) -> bool { self.time_zone().is_some() } pub fn time_zone(&self) -> Option<&FixedOffset> { - self.time_zone() - } + self.time_zone.as_ref() + }*/ } /** From 5d14d041edeb29b2d665cee7b265721fe4f1f02d Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Wed, 7 Feb 2024 20:47:38 +0100 Subject: [PATCH 05/23] - added ambiguous dt range variants --- core/Cargo.toml | 3 + core/src/value/partial.rs | 6 +- core/src/value/primitive.rs | 18 ++- core/src/value/range.rs | 236 ++++++++++++++++++------------------ 4 files changed, 133 insertions(+), 130 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index 72a0ffe36..b49dfc331 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -16,3 +16,6 @@ num-traits = "0.2.12" safe-transmute = "0.11.0" smallvec = "1.6.1" snafu = "0.7.3" + +[lib] +doctest = false \ No newline at end of file diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 9c1d98532..4613b32f5 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -1245,7 +1245,7 @@ mod tests { ) .latest() .unwrap(), - PreciseDateTimeResult::WithTimeZone( + PreciseDateTimeResult::TimeZone( FixedOffset::east_opt(0) .unwrap() .from_local_datetime(&NaiveDateTime::new( @@ -1266,7 +1266,7 @@ mod tests { .unwrap() .earliest() .unwrap(), - PreciseDateTimeResult::WithTimeZone( + PreciseDateTimeResult::TimeZone( FixedOffset::east_opt(0) .unwrap() .from_local_datetime(&NaiveDateTime::new( @@ -1286,7 +1286,7 @@ mod tests { .unwrap() .latest() .unwrap(), - PreciseDateTimeResult::WithTimeZone( + PreciseDateTimeResult::TimeZone( FixedOffset::east_opt(0) .unwrap() .from_local_datetime(&NaiveDateTime::new( diff --git a/core/src/value/primitive.rs b/core/src/value/primitive.rs index 96d0923bb..1e9ff56b0 100644 --- a/core/src/value/primitive.rs +++ b/core/src/value/primitive.rs @@ -5051,7 +5051,6 @@ mod tests { #[test] fn primitive_value_to_datetime_range() { - let offset = FixedOffset::west_opt(3600).unwrap(); assert_eq!( dicom_value!(Str, "202002-20210228153012.123") @@ -5069,25 +5068,24 @@ mod tests { ) .unwrap() ); - // East UTC offset gets parsed + // East UTC offset gets parsed but will result in an ambiguous dt-range variant assert_eq!( PrimitiveValue::from(&b"2020-2030+0800"[..]) .to_datetime_range() .unwrap(), - DateTimeRange::from_start_to_end_with_time_zone( - FixedOffset::east_opt(0) // this offset is missing, so generated - .unwrap() - .with_ymd_and_hms(2020, 1, 1, 0, 0, 0) - .unwrap(), - FixedOffset::east_opt(8 * 3600) + DateTimeRange::AmbiguousStart { + ambiguous_start: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(), + NaiveTime::from_hms_opt(0, 0, 0).unwrap() + ), + end: FixedOffset::east_opt(8 * 3600) .unwrap() .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(2030, 12, 31).unwrap(), NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() )) .unwrap() - ) - .unwrap() + } ); } diff --git a/core/src/value/range.rs b/core/src/value/range.rs index bd5423a07..f2a8e62e5 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -285,7 +285,7 @@ impl AsRange for DicomDateTime { }; match self.time_zone() { - Some(offset) => Ok(PreciseDateTimeResult::WithTimeZone( + Some(offset) => Ok(PreciseDateTimeResult::TimeZone( offset .from_local_datetime(&NaiveDateTime::new(date, time)) .single() @@ -313,7 +313,7 @@ impl AsRange for DicomDateTime { }; match self.time_zone() { - Some(offset) => Ok(PreciseDateTimeResult::WithTimeZone( + Some(offset) => Ok(PreciseDateTimeResult::TimeZone( offset .from_local_datetime(&NaiveDateTime::new(date, time)) .single() @@ -332,7 +332,7 @@ impl AsRange for DicomDateTime { if start.has_time_zone() { let s = start.into_datetime_with_time_zone()?; let e = end.into_datetime_with_time_zone()?; - Ok(DateTimeRange::WithTimeZone { + Ok(DateTimeRange::TimeZone { start: Some(s), end: Some(e), }) @@ -440,7 +440,9 @@ pub struct TimeRange { /// Represents a date-time range, that can either be time-zone naive or time-zone aware. It is stored as two [`Option>`] or /// two [`Option>`] values. /// [None] means no upper or no lower bound for range is present. -/// Please note, that this structure is guaranteed never to contain a combination of one time-zone aware and one time-zone naive value. +/// +/// In borderline cases the parser produces [DateTimeRange::AmbiguousStart] and [DateTimeRange::AmbiguousEnd]. +/// /// # Example /// ``` /// # use std::error::Error; @@ -463,27 +465,57 @@ pub struct TimeRange { /// /// assert!(dtr.start().is_some()); /// assert!(dtr.end().is_some()); -/// # Ok(()) +/// # Ok(()) /// # } /// ``` #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] pub enum DateTimeRange { + /// DateTime range without time-zone information Naive { start: Option, end: Option, }, - WithTimeZone { + /// DateTime range with time-zone information + TimeZone { start: Option>, end: Option>, }, + /// DateTime range with a lower bound value which is ambiguous and needs further interpretation. + /// This variant can only be a result of parsing a date-time range from an external source. + /// The standard allows for parsing a date-time range in which one DT value provides time-zone + /// information but the other does not. + /// + /// Example '19750101-19800101+0200'. + /// + /// In such case, the missing time-zone can be interpreted as the local time-zone or the time-zone + /// provided with the upper bound (or something else altogether). + /// And that is why this variant is not guaranteed to be monotonically ordered in time. + AmbiguousStart{ + ambiguous_start: NaiveDateTime, + end: DateTime + }, + /// DateTime range with a upper bound value which is ambiguous and needs further interpretation. + /// This variant can only be a result of parsing a date-time range from an external source. + /// The standard allows for parsing a date-time range in which one DT value provides time-zone + /// information but the other does not. + /// + /// Example '19750101+0200-19800101'. + /// + /// In such case, the missing time-zone can be interpreted as the local time-zone or the time-zone + /// provided with the lower bound (or something else altogether). + /// And that is why this variant is not guaranteed to be monotonically ordered in time. + AmbiguousEnd{ + start: DateTime, + ambiguous_end: NaiveDateTime + } } -/// A precise date-time value, that can either be time-zone aware or time-zone unaware. +/// A precise date-time value, that can either be time-zone aware or time-zone naive. /// #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd)] pub enum PreciseDateTimeResult { Naive(NaiveDateTime), - WithTimeZone(DateTime), + TimeZone(DateTime), } impl PreciseDateTimeResult { @@ -491,7 +523,7 @@ impl PreciseDateTimeResult { pub fn as_datetime_with_time_zone(&self) -> Option<&DateTime> { match self { PreciseDateTimeResult::Naive(..) => None, - PreciseDateTimeResult::WithTimeZone(value) => Some(value), + PreciseDateTimeResult::TimeZone(value) => Some(value), } } @@ -499,7 +531,7 @@ impl PreciseDateTimeResult { pub fn as_datetime(&self) -> Option<&NaiveDateTime> { match self { PreciseDateTimeResult::Naive(value) => Some(value), - PreciseDateTimeResult::WithTimeZone(..) => None, + PreciseDateTimeResult::TimeZone(..) => None, } } @@ -507,7 +539,7 @@ impl PreciseDateTimeResult { pub fn into_datetime_with_time_zone(self) -> Result> { match self { PreciseDateTimeResult::Naive(..) => NoTimeZoneSnafu.fail(), - PreciseDateTimeResult::WithTimeZone(value) => Ok(value), + PreciseDateTimeResult::TimeZone(value) => Ok(value), } } @@ -515,7 +547,7 @@ impl PreciseDateTimeResult { pub fn into_datetime(self) -> Result { match self { PreciseDateTimeResult::Naive(value) => Ok(value), - PreciseDateTimeResult::WithTimeZone(..) => DateTimeTzAwareSnafu.fail(), + PreciseDateTimeResult::TimeZone(..) => DateTimeTzAwareSnafu.fail(), } } @@ -523,7 +555,7 @@ impl PreciseDateTimeResult { pub fn has_time_zone(&self) -> bool { match self { PreciseDateTimeResult::Naive(..) => false, - PreciseDateTimeResult::WithTimeZone(..) => true, + PreciseDateTimeResult::TimeZone(..) => true, } } } @@ -621,7 +653,7 @@ impl TimeRange { } impl DateTimeRange { - /// Constructs a new `DateTimeRange` from two `chrono::DateTime` values + /// Constructs a new time-zone aware `DateTimeRange` from two `chrono::DateTime` values /// monotonically ordered in time. pub fn from_start_to_end_with_time_zone( start: DateTime, @@ -634,14 +666,14 @@ impl DateTimeRange { } .fail() } else { - Ok(DateTimeRange::WithTimeZone { + Ok(DateTimeRange::TimeZone { start: Some(start), end: Some(end), }) } } - /// Constructs a new `DateTimeRange` from two `chrono::NaiveDateTime` values + /// Constructs a new time-zone naive `DateTimeRange` from two `chrono::NaiveDateTime` values /// monotonically ordered in time. pub fn from_start_to_end(start: NaiveDateTime, end: NaiveDateTime) -> Result { if start > end { @@ -658,16 +690,16 @@ impl DateTimeRange { } } - /// Constructs a new `DateTimeRange` beginning with a `chrono::DateTime` value + /// Constructs a new time-zone aware `DateTimeRange` beginning with a `chrono::DateTime` value /// and no upper limit. pub fn from_start_with_time_zone(start: DateTime) -> DateTimeRange { - DateTimeRange::WithTimeZone { + DateTimeRange::TimeZone { start: Some(start), end: None, } } - /// Constructs a new `DateTimeRange` beginning with a `chrono::NaiveDateTime` value + /// Constructs a new time-zone naive `DateTimeRange` beginning with a `chrono::NaiveDateTime` value /// and no upper limit. pub fn from_start(start: NaiveDateTime) -> DateTimeRange { DateTimeRange::Naive { @@ -676,15 +708,15 @@ impl DateTimeRange { } } - /// Constructs a new `DateTimeRange` with no lower limit, ending with a `chrono::DateTime` value. + /// Constructs a new time-zone aware `DateTimeRange` with no lower limit, ending with a `chrono::DateTime` value. pub fn from_end_with_time_zone(end: DateTime) -> DateTimeRange { - DateTimeRange::WithTimeZone { + DateTimeRange::TimeZone { start: None, end: Some(end), } } - /// Constructs a new `DateTimeRange` with no lower limit, ending with a `chrono::NaiveDateTime` value. + /// Constructs a new time-zone naive `DateTimeRange` with no lower limit, ending with a `chrono::NaiveDateTime` value. pub fn from_end(end: NaiveDateTime) -> DateTimeRange { DateTimeRange::Naive { start: None, @@ -693,22 +725,34 @@ impl DateTimeRange { } /// Returns the lower bound of the range. + /// + /// If the date-time range contains an ambiguous lower bound value, it will return `None`. + /// See [DateTimeRange::AmbiguousStart] pub fn start(&self) -> Option { match self { DateTimeRange::Naive { start, .. } => start.map(|dt| PreciseDateTimeResult::Naive(dt)), - DateTimeRange::WithTimeZone { start, .. } => { - start.map(|dt| PreciseDateTimeResult::WithTimeZone(dt)) + DateTimeRange::TimeZone { start, .. } => { + start.map(|dt| PreciseDateTimeResult::TimeZone(dt)) } + DateTimeRange::AmbiguousStart { .. } => None, + DateTimeRange::AmbiguousEnd { start, .. } => Some(PreciseDateTimeResult::TimeZone(start.clone())), + } + } /// Returns the upper bound of the range. + /// + /// If the date-time range contains an ambiguous upper bound value, it will return `None`. + /// See [DateTimeRange::AmbiguousEnd] pub fn end(&self) -> Option { match self { - DateTimeRange::Naive { start, end } => end.map(|dt| PreciseDateTimeResult::Naive(dt)), - DateTimeRange::WithTimeZone { start, end } => { - end.map(|dt| PreciseDateTimeResult::WithTimeZone(dt)) - } + DateTimeRange::Naive { start: _, end } => end.map(|dt| PreciseDateTimeResult::Naive(dt)), + DateTimeRange::TimeZone { start: _, end } => { + end.map(|dt| PreciseDateTimeResult::TimeZone(dt)) + }, + DateTimeRange::AmbiguousStart { ambiguous_start: _, end } => Some(PreciseDateTimeResult::TimeZone(end.clone())), + DateTimeRange::AmbiguousEnd { .. } => None, } } @@ -755,13 +799,12 @@ impl DateTimeRange { } } - /*pub fn has_time_zone(&self) -> bool { - self.time_zone().is_some() + pub fn is_ambiguous(&self) -> bool { + match self{ + DateTimeRange::AmbiguousEnd { .. } | &DateTimeRange::AmbiguousStart { .. } => true, + _ => false + } } - - pub fn time_zone(&self) -> Option<&FixedOffset> { - self.time_zone.as_ref() - }*/ } /** @@ -837,8 +880,7 @@ pub fn parse_time_range(buf: &[u8]) -> Result { /// Looks for a range separator '-'. /// Returns a `DateTimeRange`. /// If the parser encounters two date-time values, where one is time-zone aware and the other is not, -/// it will automatically convert the time-zone naive value to a time-zone aware value with zero east offset. -/// (same as appending '+0000' to the text representation). +/// it will produce an ambiguous DateTimeRange variant. /// Users are advised, that for very specific inputs, inconsistent behavior can occur. /// This behavior can only be produced when all of the following is true: /// - two very short date-times in the form of YYYY are presented (YYYY-YYYY) @@ -857,7 +899,7 @@ pub fn parse_datetime_range(buf: &[u8]) -> Result { let buf = &buf[1..]; match parse_datetime_partial(buf).context(ParseSnafu)?.latest()? { PreciseDateTimeResult::Naive(end) => Ok(DateTimeRange::from_end(end)), - PreciseDateTimeResult::WithTimeZone(end_tz) => { + PreciseDateTimeResult::TimeZone(end_tz) => { Ok(DateTimeRange::from_end_with_time_zone(end_tz)) } } @@ -869,7 +911,7 @@ pub fn parse_datetime_range(buf: &[u8]) -> Result { .earliest()? { PreciseDateTimeResult::Naive(start) => Ok(DateTimeRange::from_start(start)), - PreciseDateTimeResult::WithTimeZone(start_tz) => { + PreciseDateTimeResult::TimeZone(start_tz) => { Ok(DateTimeRange::from_start_with_time_zone(start_tz)) } } @@ -903,38 +945,22 @@ pub fn parse_datetime_range(buf: &[u8]) -> Result { PreciseDateTimeResult::Naive(end), ) => DateTimeRange::from_start_to_end(start, end), ( - PreciseDateTimeResult::WithTimeZone(start), - PreciseDateTimeResult::WithTimeZone(end), + PreciseDateTimeResult::TimeZone(start), + PreciseDateTimeResult::TimeZone(end), ) => DateTimeRange::from_start_to_end_with_time_zone(start, end), ( + // lower bound time-zone was missing PreciseDateTimeResult::Naive(start), - PreciseDateTimeResult::WithTimeZone(end), + PreciseDateTimeResult::TimeZone(end), ) => { - let offset = FixedOffset::east_opt(0).unwrap(); - DateTimeRange::from_start_to_end_with_time_zone( - offset.from_local_datetime(&start).single().context( - InvalidDateTimeSnafu { - naive: start, - offset: offset, - }, - )?, - end, - ) + Ok(DateTimeRange::AmbiguousStart { ambiguous_start: start, end }) } ( - PreciseDateTimeResult::WithTimeZone(start), + PreciseDateTimeResult::TimeZone(start), + // upper bound time-zone was missing PreciseDateTimeResult::Naive(end), ) => { - let offset = FixedOffset::east_opt(0).unwrap(); - DateTimeRange::from_start_to_end_with_time_zone( - start, - offset.from_local_datetime(&end).single().context( - InvalidDateTimeSnafu { - naive: end, - offset: offset, - }, - )?, - ) + Ok(DateTimeRange::AmbiguousEnd { start, ambiguous_end: end }) } }; match dtr { @@ -962,34 +988,16 @@ pub fn parse_datetime_range(buf: &[u8]) -> Result { DateTimeRange::from_start_to_end(start, end) } ( - PreciseDateTimeResult::WithTimeZone(start), - PreciseDateTimeResult::WithTimeZone(end), + PreciseDateTimeResult::TimeZone(start), + PreciseDateTimeResult::TimeZone(end), ) => DateTimeRange::from_start_to_end_with_time_zone(start, end), - (PreciseDateTimeResult::Naive(start), PreciseDateTimeResult::WithTimeZone(end)) => { - let offset = FixedOffset::east_opt(0).unwrap(); - DateTimeRange::from_start_to_end_with_time_zone( - offset - .from_local_datetime(&start) - .single() - .context(InvalidDateTimeSnafu { - naive: start, - offset: offset, - })?, - end, - ) + // lower bound time-zone was missing + (PreciseDateTimeResult::Naive(start), PreciseDateTimeResult::TimeZone(end)) => { + Ok(DateTimeRange::AmbiguousStart { ambiguous_start: start, end }) } - (PreciseDateTimeResult::WithTimeZone(start), PreciseDateTimeResult::Naive(end)) => { - let offset = FixedOffset::east_opt(0).unwrap(); - DateTimeRange::from_start_to_end_with_time_zone( - start, - offset - .from_local_datetime(&end) - .single() - .context(InvalidDateTimeSnafu { - naive: end, - offset: offset, - })?, - ) + // upper bound time-zone was missing + (PreciseDateTimeResult::TimeZone(start), PreciseDateTimeResult::Naive(end)) => { + Ok(DateTimeRange::AmbiguousEnd { start, ambiguous_end: end }) } } } @@ -1089,7 +1097,7 @@ mod tests { .unwrap() ) .start(), - Some(PreciseDateTimeResult::WithTimeZone( + Some(PreciseDateTimeResult::TimeZone( offset .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), @@ -1109,7 +1117,7 @@ mod tests { .unwrap() ) .end(), - Some(PreciseDateTimeResult::WithTimeZone( + Some(PreciseDateTimeResult::TimeZone( offset .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), @@ -1136,7 +1144,7 @@ mod tests { ) .unwrap() .start(), - Some(PreciseDateTimeResult::WithTimeZone( + Some(PreciseDateTimeResult::TimeZone( offset .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), @@ -1162,7 +1170,7 @@ mod tests { ) .unwrap() .end(), - Some(PreciseDateTimeResult::WithTimeZone( + Some(PreciseDateTimeResult::TimeZone( offset .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), @@ -1489,7 +1497,7 @@ mod tests { // two 'east' UTC offsets get parsed assert_eq!( parse_datetime_range(b"19900101+0500-1999+1400").ok(), - Some(DateTimeRange::WithTimeZone { + Some(DateTimeRange::TimeZone { start: Some( FixedOffset::east_opt(5 * 3600) .unwrap() @@ -1513,7 +1521,7 @@ mod tests { // two 'west' Time zone offsets get parsed assert_eq!( parse_datetime_range(b"19900101-0500-1999-1200").ok(), - Some(DateTimeRange::WithTimeZone { + Some(DateTimeRange::TimeZone { start: Some( FixedOffset::west_opt(5 * 3600) .unwrap() @@ -1537,7 +1545,7 @@ mod tests { // 'east' and 'west' Time zone offsets get parsed assert_eq!( parse_datetime_range(b"19900101+1400-1999-1200").ok(), - Some(DateTimeRange::WithTimeZone { + Some(DateTimeRange::TimeZone { start: Some( FixedOffset::east_opt(14 * 3600) .unwrap() @@ -1559,11 +1567,11 @@ mod tests { }) ); // one 'west' Time zone offset gets parsed, offset cannot be mistaken for a date-time - // the missing Time zone offset of the upper bound is generated as a east zero offset. + // the missing Time zone offset will result in an ambiguous range variant assert_eq!( parse_datetime_range(b"19900101-1200-1999").unwrap(), - DateTimeRange::WithTimeZone { - start: Some( + DateTimeRange::AmbiguousEnd { + start: FixedOffset::west_opt(12 * 3600) .unwrap() .from_local_datetime(&NaiveDateTime::new( @@ -1571,34 +1579,28 @@ mod tests { NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() )) .unwrap() - ), - end: Some( - FixedOffset::east_opt(0) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( + , + ambiguous_end: + NaiveDateTime::new( NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(), NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() - )) - .unwrap() - ) + ) + } ); // '0500' can either be a valid west UTC offset on the lower bound, or a valid date-time on the upper bound // Now, the first dash is considered to be a range separator, so the lower bound time-zone offset is missing - // and will be generated to be a east zero offset. + // and will result in an ambiguous range variant. assert_eq!( parse_datetime_range(b"0050-0500-1000").unwrap(), - DateTimeRange::WithTimeZone { - start: Some( - FixedOffset::east_opt(0) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( + DateTimeRange::AmbiguousStart { + ambiguous_start: + NaiveDateTime::new( NaiveDate::from_ymd_opt(50, 1, 1).unwrap(), NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() - )) - .unwrap() - ), - end: Some( + ) + , + end: FixedOffset::west_opt(10 * 3600) .unwrap() .from_local_datetime(&NaiveDateTime::new( @@ -1606,7 +1608,7 @@ mod tests { NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() )) .unwrap() - ) + } ); // sequence with more than 3 dashes '-' is refused. From c49f653fb5f015fafeda36d6897ab58a5dee3ce8 Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Wed, 7 Feb 2024 22:08:38 +0100 Subject: [PATCH 06/23] - remove Precision trait ? --- core/Cargo.toml | 4 +- core/src/value/mod.rs | 2 +- core/src/value/partial.rs | 9 +++-- core/src/value/range.rs | 83 ++++++++++++++++++++++++++++++++++----- 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index b49dfc331..e93f80174 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -17,5 +17,5 @@ safe-transmute = "0.11.0" smallvec = "1.6.1" snafu = "0.7.3" -[lib] -doctest = false \ No newline at end of file +#[lib] +#doctest = false \ No newline at end of file diff --git a/core/src/value/mod.rs b/core/src/value/mod.rs index 7fe305c6c..8d1283185 100644 --- a/core/src/value/mod.rs +++ b/core/src/value/mod.rs @@ -14,7 +14,7 @@ pub mod range; pub mod serialize; pub use self::deserialize::Error as DeserializeError; -pub use self::partial::{DicomDate, DicomDateTime, DicomTime, Precision}; +pub use self::partial::{DicomDate, DicomDateTime, DicomTime}; pub use self::person_name::PersonName; pub use self::range::{AsRange, DateRange, DateTimeRange, TimeRange, PreciseDateTimeResult}; diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 4613b32f5..58b91624a 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -5,6 +5,7 @@ use snafu::{Backtrace, ResultExt, Snafu}; use std::convert::{TryFrom, TryInto}; use std::fmt; use std::ops::RangeInclusive; +use crate::value::AsRange; #[derive(Debug, Snafu)] #[non_exhaustive] @@ -91,7 +92,7 @@ pub enum DateComponent { /// # use std::error::Error; /// # use std::convert::TryFrom; /// use chrono::NaiveDate; -/// use dicom_core::value::{DicomDate, AsRange}; +/// use dicom_core::value::{DicomDate, AsRange, Precision}; /// # fn main() -> Result<(), Box> { /// /// let date = DicomDate::from_y(1492)?; @@ -786,7 +787,7 @@ impl fmt::Debug for DicomDateTime { } -/// This trait is implemented by partial precision date, time and date-time structures. +/*/// This trait is implemented by partial precision date, time and date-time structures. /// This is useful to easily determine if the date / time value is precise without calling more expensive /// methods first. /// [Precision::precision()] method will retrieve the last fully precise component of it's stored date / time value. @@ -847,7 +848,7 @@ impl fmt::Debug for DicomDateTime { pub trait Precision { /// will retrieve the last fully precise component of a date / time structure fn precision(&self) -> DateComponent; - /// returns true if value has all possible components + /// returns true if value has all possible date / time components fn is_precise(&self) -> bool; } @@ -897,7 +898,7 @@ impl Precision for DicomDateTime { None => false } } -} +}*/ impl DicomDate { /** diff --git a/core/src/value/range.rs b/core/src/value/range.rs index f2a8e62e5..8ec3e0cc9 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -2,14 +2,14 @@ //! Parsing into ranges happens via partial precision structures (DicomDate, DicomTime, //! DicomDatime) so ranges can handle null components in date, time, date-time values. use chrono::{ - offset, DateTime, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, + DateTime, FixedOffset, LocalResult, 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::{DateComponent, DicomDate, DicomDateTime, DicomTime, Precision}; +use crate::value::partial::{DateComponent, DicomDate, DicomDateTime, DicomTime}; #[derive(Debug, Snafu)] #[non_exhaustive] @@ -85,14 +85,18 @@ pub enum Error { } type Result = std::result::Result; -/// The DICOM protocol accepts date / time values with null components. +/// The DICOM protocol accepts date (DA) / time(TM) / date-time(DT) values with null components. /// /// Imprecise values are to be handled as date / time ranges. /// /// This trait is implemented by date / time structures with partial precision. -/// If the date / time structure is not precise, it is up to the user to call one of these -/// methods to retrieve a suitable [`chrono`] value. /// +/// [Precision::is_precise()] method will check if the given value has full precision. If so, it can be +/// converted with [AsRange::exact()] to a `chrono` value. If not, [AsRange::range()] will yield a +/// date / time range. +/// +/// Please note that precision does not equal validity. A precise 'YYYYMMDD' [DicomDate] can still +/// fail to produce a valid [chrono::NaiveDate] /// /// # Examples /// @@ -123,13 +127,51 @@ type Result = std::result::Result; /// ); /// // only a time with 6 digits second fraction is considered precise /// assert!(dicom_time.exact().is_err()); +/// +/// let primitive = PrimitiveValue::from("199402"); +/// +/// // This is the fastest way to get to a useful value, but it fails not only for invalid +/// // dates but for imprecise ones as well. +/// assert!(primitive.to_naive_date().is_err()); +/// +/// // We should take indermediary steps ... +/// +/// // The parser now checks for basic year and month value ranges here. +/// // But, it would not detect invalid dates like 30th of february etc. +/// let dicom_date : DicomDate = primitive.to_date()?; +/// +/// // now we have a valid DicomDate value, let's check if it's precise. +/// if dicom_date.is_precise(){ +/// // no components are missing, we can proceed by calling .exact() +/// // which calls the `chrono` library +/// let precise_date: NaiveDate = dicom_date.exact()?; +/// } +/// else{ +/// // day and / or month are missing, no need to call expensive .exact() method +/// // - it will fail +/// // try to retrieve the date range instead +/// let date_range: DateRange = dicom_date.range()?; +/// +/// // the real conversion to a `chrono` value only happens at this stage +/// if let Some(start) = date_range.start(){ +/// // the range has a given lower date bound +/// } +/// +/// // or try to retrieve the earliest possible value directly from DicomDate +/// let earliest: NaiveDate = dicom_date.earliest()?; +/// +/// } /// /// # Ok(()) /// # } /// ``` -pub trait AsRange: Precision { +pub trait AsRange { type PreciseValue: PartialEq + PartialOrd; type Range; + + /// returns true if value has all possible date / time components + fn is_precise(&self) -> bool; + /// Returns a corresponding `chrono` value, if the partial precision structure has full accuracy. fn exact(&self) -> Result { if self.is_precise() { @@ -156,6 +198,11 @@ pub trait AsRange: Precision { impl AsRange for DicomDate { type PreciseValue = NaiveDate; type Range = DateRange; + + fn is_precise(&self) -> bool { + self.day().is_some() + } + fn earliest(&self) -> Result { let (y, m, d) = { ( @@ -222,6 +269,14 @@ impl AsRange for DicomDate { impl AsRange for DicomTime { type PreciseValue = NaiveTime; type Range = TimeRange; + + fn is_precise(&self) -> bool { + match self.fraction_and_precision(){ + Some((fr_, precision)) if precision == &6 => true, + _ => false + } + } + fn earliest(&self) -> Result { let (h, m, s, f) = ( self.hour(), @@ -273,6 +328,14 @@ impl AsRange for DicomTime { impl AsRange for DicomDateTime { type PreciseValue = PreciseDateTimeResult; type Range = DateTimeRange; + + fn is_precise(&self) -> bool { + match self.time(){ + Some(dicom_time) => dicom_time.is_precise(), + None => false + } + } + fn earliest(&self) -> Result { let date = self.date().earliest()?; let time = match self.time() { @@ -361,9 +424,11 @@ impl DicomTime { /// /// Missing second fraction defaults to zero. pub fn to_naive_time(self) -> Result { - match self.precision() { - DateComponent::Second | DateComponent::Fraction => self.earliest(), - _ => ImpreciseValueSnafu.fail(), + if self.second().is_some() { + self.earliest() + } + else{ + ImpreciseValueSnafu.fail() } } } From 1b9ae35888503531587966b2c7c6cf7cb14c0cd7 Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Thu, 8 Feb 2024 20:53:58 +0100 Subject: [PATCH 07/23] - fix doc test - deprecation messages - add basic D/T values to prelude - clippy fixes --- core/src/header.rs | 1 - core/src/prelude.rs | 1 + core/src/value/deserialize.rs | 181 +--------------- core/src/value/mod.rs | 3 - core/src/value/partial.rs | 239 ++++++--------------- core/src/value/primitive.rs | 392 ++++++---------------------------- core/src/value/range.rs | 384 +++++++++++++++------------------ parser/src/stateful/decode.rs | 1 - 8 files changed, 312 insertions(+), 890 deletions(-) diff --git a/core/src/header.rs b/core/src/header.rs index e3763adb7..bcc254b68 100644 --- a/core/src/header.rs +++ b/core/src/header.rs @@ -6,7 +6,6 @@ use crate::value::{ CastValueError, ConvertValueError, DataSetSequence, DicomDate, DicomDateTime, DicomTime, InMemFragment, PrimitiveValue, Value, C, }; -use chrono::FixedOffset; use num_traits::NumCast; use snafu::{ensure, Backtrace, Snafu}; use std::borrow::Cow; diff --git a/core/src/prelude.rs b/core/src/prelude.rs index 74d6673a2..cc37e3218 100644 --- a/core/src/prelude.rs +++ b/core/src/prelude.rs @@ -10,3 +10,4 @@ pub use crate::{dicom_value, DataElement, DicomValue, Tag, VR}; pub use crate::{header::HasLength as _, DataDictionary as _}; +pub use crate::value::{AsRange as _, DicomDate, DicomTime, DicomDateTime}; diff --git a/core/src/value/deserialize.rs b/core/src/value/deserialize.rs index 9ddafd7a3..2f504a88e 100644 --- a/core/src/value/deserialize.rs +++ b/core/src/value/deserialize.rs @@ -337,10 +337,7 @@ where * For DateTime with missing components, or if exact second fraction accuracy needs to be preserved, use `parse_datetime_partial`. */ -#[deprecated( - note = "As time-zone aware an naive DicomDateTime was introduced, this method could only access the time-zone - aware values, which would lead to confusing behavior." -)] +#[deprecated(since = "0.6.4", note = "Only use parse_datetime_partial()")] pub fn parse_datetime(buf: &[u8], dt_utc_offset: FixedOffset) -> Result> { let date = parse_date(buf)?; let buf = &buf[8..]; @@ -435,7 +432,7 @@ pub fn parse_datetime_partial(buf: &[u8]) -> Result { #[cfg(test)] mod tests { use super::*; - use chrono::{FixedOffset, NaiveDate, NaiveTime, TimeZone}; + use chrono::{FixedOffset, NaiveDate, NaiveTime}; #[test] fn test_parse_date() { @@ -789,183 +786,9 @@ mod tests { }) )); } - #[test] - fn test_parse_datetime() { - let default_offset = FixedOffset::east_opt(0).unwrap(); - assert_eq!( - parse_datetime(b"20171130101010.204", default_offset).unwrap(), - FixedOffset::east_opt(0) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2017, 11, 30).unwrap(), - NaiveTime::from_hms_micro_opt(10, 10, 10, 204_000).unwrap() - )) - .unwrap() - ); - assert_eq!( - parse_datetime(b"19440229101010.1", default_offset).unwrap(), - FixedOffset::east_opt(0) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(1944, 2, 29).unwrap(), - NaiveTime::from_hms_micro_opt(10, 10, 10, 100_000).unwrap() - )) - .unwrap() - ); - assert_eq!( - parse_datetime(b"19450228101010.999999", default_offset).unwrap(), - FixedOffset::east_opt(0) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(1945, 2, 28).unwrap(), - NaiveTime::from_hms_micro_opt(10, 10, 10, 999_999).unwrap() - )) - .unwrap() - ); - assert_eq!( - parse_datetime(b"20171130101010.564204-1001", default_offset).unwrap(), - FixedOffset::west_opt(10 * 3600 + 1 * 60) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2017, 11, 30).unwrap(), - NaiveTime::from_hms_micro_opt(10, 10, 10, 564_204).unwrap() - )) - .unwrap() - ); - assert_eq!( - parse_datetime(b"20171130101010.564204-1001abcd", default_offset).unwrap(), - FixedOffset::west_opt(10 * 3600 + 1 * 60) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2017, 11, 30).unwrap(), - NaiveTime::from_hms_micro_opt(10, 10, 10, 564_204).unwrap() - )) - .unwrap() - ); - assert_eq!( - parse_datetime(b"20171130101010.2-1100", default_offset).unwrap(), - FixedOffset::west_opt(11 * 3600) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2017, 11, 30).unwrap(), - NaiveTime::from_hms_micro_opt(10, 10, 10, 200_000).unwrap() - )) - .unwrap() - ); - assert_eq!( - parse_datetime(b"20171130101010.0-1100", default_offset).unwrap(), - FixedOffset::west_opt(11 * 3600) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2017, 11, 30).unwrap(), - NaiveTime::from_hms_micro_opt(10, 10, 10, 0).unwrap() - )) - .unwrap() - ); - assert_eq!( - parse_datetime(b"20180101093059", default_offset).unwrap(), - FixedOffset::east_opt(0) - .unwrap() - .with_ymd_and_hms(2018, 1, 1, 9, 30, 59) - .unwrap() - ); - assert!(matches!( - parse_datetime(b"201801010930", default_offset), - Err(Error::IncompleteValue { - component: DateComponent::Second, - .. - }) - )); - assert!(matches!( - parse_datetime(b"2018010109", default_offset), - Err(Error::IncompleteValue { - component: DateComponent::Minute, - .. - }) - )); - assert!(matches!( - parse_datetime(b"20180101", default_offset), - Err(Error::UnexpectedEndOfElement { .. }) - )); - assert!(matches!( - parse_datetime(b"201801", default_offset), - Err(Error::IncompleteValue { - component: DateComponent::Day, - .. - }) - )); - assert!(matches!( - parse_datetime(b"1526", default_offset), - Err(Error::IncompleteValue { - component: DateComponent::Month, - .. - }) - )); - - let dt = parse_datetime(b"20171130101010.204+0100", default_offset).unwrap(); - assert_eq!( - dt, - FixedOffset::east_opt(3600) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2017, 11, 30).unwrap(), - NaiveTime::from_hms_micro_opt(10, 10, 10, 204_000).unwrap() - )) - .unwrap() - ); - assert_eq!( - format!("{:?}", dt), - "2017-11-30T10:10:10.204+01:00".to_string() - ); - - let dt = parse_datetime(b"20171130101010.204+0535", default_offset).unwrap(); - assert_eq!( - dt, - FixedOffset::east_opt(5 * 3600 + 35 * 60) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2017, 11, 30).unwrap(), - NaiveTime::from_hms_micro_opt(10, 10, 10, 204_000).unwrap() - )) - .unwrap() - ); - assert_eq!( - format!("{:?}", dt), - "2017-11-30T10:10:10.204+05:35".to_string() - ); - assert_eq!( - parse_datetime(b"20140505120101.204+0535", default_offset).unwrap(), - FixedOffset::east_opt(5 * 3600 + 35 * 60) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2014, 5, 5).unwrap(), - NaiveTime::from_hms_micro_opt(12, 1, 1, 204_000).unwrap() - )) - .unwrap() - ); - - assert!(parse_datetime(b"", default_offset).is_err()); - assert!(parse_datetime(&[0x00_u8; 8], default_offset).is_err()); - assert!(parse_datetime(&[0xFF_u8; 8], default_offset).is_err()); - assert!(parse_datetime(&[b'0'; 8], default_offset).is_err()); - assert!(parse_datetime(&[b' '; 8], default_offset).is_err()); - assert!(parse_datetime(b"nope", default_offset).is_err()); - assert!(parse_datetime(b"2015dec", default_offset).is_err()); - assert!(parse_datetime(b"20151231162945.", default_offset).is_err()); - assert!(parse_datetime(b"20151130161445+", default_offset).is_err()); - assert!(parse_datetime(b"20151130161445+----", default_offset).is_err()); - assert!(parse_datetime(b"20151130161445. ", default_offset).is_err()); - assert!(parse_datetime(b"20151130161445. +0000", default_offset).is_err()); - assert!(parse_datetime(b"20100423164000.001+3", default_offset).is_err()); - assert!(parse_datetime(b"200809112945*1000", default_offset).is_err()); - assert!(parse_datetime(b"20171130101010.204+1", default_offset).is_err()); - assert!(parse_datetime(b"20171130101010.204+01", default_offset).is_err()); - assert!(parse_datetime(b"20171130101010.204+011", default_offset).is_err()); - } #[test] fn test_parse_datetime_partial() { - let default_offset = FixedOffset::east_opt(0).unwrap(); assert_eq!( parse_datetime_partial(b"20171130101010.204").unwrap(), DicomDateTime::from_date_and_time( diff --git a/core/src/value/mod.rs b/core/src/value/mod.rs index 8d1283185..0d4e54bcf 100644 --- a/core/src/value/mod.rs +++ b/core/src/value/mod.rs @@ -23,9 +23,6 @@ pub use self::primitive::{ ValueType, }; -/// re-exported from chrono -use chrono::FixedOffset; - /// An aggregation of one or more elements in a value. pub type C = SmallVec<[T; 2]>; diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 58b91624a..57d1e906d 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -1,11 +1,11 @@ //! Handling of partial precision of Date, Time and DateTime values. +use crate::value::AsRange; use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; use snafu::{Backtrace, ResultExt, Snafu}; use std::convert::{TryFrom, TryInto}; use std::fmt; use std::ops::RangeInclusive; -use crate::value::AsRange; #[derive(Debug, Snafu)] #[non_exhaustive] @@ -92,7 +92,7 @@ pub enum DateComponent { /// # use std::error::Error; /// # use std::convert::TryFrom; /// use chrono::NaiveDate; -/// use dicom_core::value::{DicomDate, AsRange, Precision}; +/// use dicom_core::value::{DicomDate, AsRange}; /// # fn main() -> Result<(), Box> { /// /// let date = DicomDate::from_y(1492)?; @@ -207,7 +207,7 @@ enum DicomTimeImpl { /// // the earliest possible value is output as a [PreciseDateTimeResult] /// assert_eq!( /// dt.earliest()?, -/// PreciseDateTimeResult::WithTimeZone( +/// PreciseDateTimeResult::TimeZone( /// offset.from_local_datetime(&NaiveDateTime::new( /// NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(), /// NaiveTime::from_hms_opt(0, 0, 0).unwrap() @@ -215,7 +215,7 @@ enum DicomTimeImpl { /// ); /// assert_eq!( /// dt.latest()?, -/// PreciseDateTimeResult::WithTimeZone( +/// PreciseDateTimeResult::TimeZone( /// offset.from_local_datetime(&NaiveDateTime::new( /// NaiveDate::from_ymd_opt(2020, 12, 31).unwrap(), /// NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() @@ -328,6 +328,15 @@ impl DicomDate { DicomDate(DicomDateImpl::Day(_, _, d)) => Some(d), } } + + /** Rertrieves the last fully precise `DateComponent` of the value */ + pub(crate) fn precision(&self) -> DateComponent { + match self { + DicomDate(DicomDateImpl::Year(..)) => DateComponent::Year, + DicomDate(DicomDateImpl::Month(..)) => DateComponent::Month, + DicomDate(DicomDateImpl::Day(..)) => DateComponent::Day, + } + } } impl TryFrom<&NaiveDate> for DicomDate { @@ -515,6 +524,16 @@ impl DicomTime { frac_precision, ))) } + + /** Rertrieves the last fully precise `DateComponent` of the value */ + pub(crate) fn precision(&self) -> DateComponent { + match self { + DicomTime(DicomTimeImpl::Hour(..)) => DateComponent::Hour, + DicomTime(DicomTimeImpl::Minute(..)) => DateComponent::Minute, + DicomTime(DicomTimeImpl::Second(..)) => DateComponent::Second, + DicomTime(DicomTimeImpl::Fraction(..)) => DateComponent::Fraction, + } + } } impl TryFrom<&NaiveTime> for DicomTime { @@ -669,6 +688,10 @@ impl DicomDateTime { pub fn has_time_zone(&self) -> bool { self.time_zone.is_some() } + + /** Retrieves a refrence to the internal offset value */ + #[deprecated(since = "0.6.4", note = "Use `time_zone` instead")] + pub fn offset(&self) {} } impl TryFrom<&DateTime> for DicomDateTime { @@ -786,120 +809,6 @@ impl fmt::Debug for DicomDateTime { } } - -/*/// This trait is implemented by partial precision date, time and date-time structures. -/// This is useful to easily determine if the date / time value is precise without calling more expensive -/// methods first. -/// [Precision::precision()] method will retrieve the last fully precise component of it's stored date / time value. -/// [Precision::is_precise()] method will check if the given value has full precision. If so, it can be -/// converted with [AsRange::exact()] to a `chrono` value. If not, [AsRange::range()] will yield a -/// date / time range. -/// -/// Please note that precision does not equal validity. A precise 'YYYYMMDD' [DicomDate] can still -/// fail to produce a valid [chrono::NaiveDate] -/// -/// # Example -/// -/// ``` -/// # use dicom_core::value::{C, PrimitiveValue}; -/// use chrono::{NaiveDate}; -/// # use std::error::Error; -/// use dicom_core::value::{DateRange, DicomDate, AsRange, Precision}; -/// # fn main() -> Result<(), Box> { -/// -/// let primitive = PrimitiveValue::from("199402"); -/// -/// // the fastest way to get to a useful value, but it fails not only for invalid -/// // dates but for imprecise ones as well. -/// assert!(primitive.to_naive_date().is_err()); -/// -/// // We should take indermediary steps ... -/// -/// // The parser now checks for basic year and month value ranges here. -/// // But, it would not detect invalid dates like 30th of february etc. -/// let dicom_date : DicomDate = primitive.to_date()?; -/// -/// // now we have a valid DicomDate value, let's check if it's precise. -/// if dicom_date.is_precise(){ -/// // no components are missing, we can proceed by calling .exact() -/// // which calls the `chrono` library -/// let precise_date: NaiveDate = dicom_date.exact()?; -/// } -/// else{ -/// // day and / or month are missing, no need to call expensive .exact() method -/// // - it will fail -/// // try to retrieve the date range instead -/// let date_range: DateRange = dicom_date.range()?; -/// -/// // the real conversion to a `chrono` value only happens at this stage -/// if let Some(start) = date_range.start(){ -/// // the range has a given lower date bound -/// } -/// -/// // or try to retrieve the earliest possible value directly from DicomDate -/// let earliest: NaiveDate = dicom_date.earliest()?; -/// -/// } -/// -/// -/// # Ok(()) -/// # } -/// ``` -pub trait Precision { - /// will retrieve the last fully precise component of a date / time structure - fn precision(&self) -> DateComponent; - /// returns true if value has all possible date / time components - fn is_precise(&self) -> bool; -} - -impl Precision for DicomDate { - fn precision(&self) -> DateComponent { - match self { - DicomDate(DicomDateImpl::Year(..)) => DateComponent::Year, - DicomDate(DicomDateImpl::Month(..)) => DateComponent::Month, - DicomDate(DicomDateImpl::Day(..)) => DateComponent::Day, - } - } - fn is_precise(&self) -> bool { - match self{ - DicomDate(DicomDateImpl::Day(..)) => true, - _ => false - } - } -} - -impl Precision for DicomTime { - fn precision(&self) -> DateComponent { - match self { - DicomTime(DicomTimeImpl::Hour(..)) => DateComponent::Hour, - DicomTime(DicomTimeImpl::Minute(..)) => DateComponent::Minute, - DicomTime(DicomTimeImpl::Second(..)) => DateComponent::Second, - DicomTime(DicomTimeImpl::Fraction(..)) => DateComponent::Fraction, - } - } - fn is_precise(&self) -> bool { - match self.fraction_and_precision() { - Some((_,fraction_precision)) if fraction_precision == &6 => true, - _ => false - } - } -} - -impl Precision for DicomDateTime { - fn precision(&self) -> DateComponent { - match self.time { - Some(time) => time.precision(), - None => self.date.precision(), - } - } - fn is_precise(&self) -> bool { - match self.time(){ - Some(time) => time.is_precise(), - None => false - } - } -}*/ - impl DicomDate { /** * Retrieves a dicom encoded string representation of the value. @@ -960,7 +869,7 @@ impl DicomDateTime { self.date.to_encoded(), offset.to_string().replace(':', "") ), - None => format!("{}", self.date.to_encoded()), + None => self.date.to_encoded().to_string(), }, } } @@ -968,7 +877,7 @@ impl DicomDateTime { #[cfg(test)] mod tests { - use crate::value::range::{AsRange,PreciseDateTimeResult}; + use crate::value::range::{AsRange, PreciseDateTimeResult}; use super::*; use chrono::{NaiveDateTime, TimeZone}; @@ -979,11 +888,9 @@ mod tests { DicomDate::from_ymd(1944, 2, 29).unwrap(), DicomDate(DicomDateImpl::Day(1944, 2, 29)) ); - + // cheap precision check, but date is invalid - assert!( - DicomDate::from_ymd(1945, 2, 29).unwrap().is_precise() - ); + assert!(DicomDate::from_ymd(1945, 2, 29).unwrap().is_precise()); assert_eq!( DicomDate::from_ym(1944, 2).unwrap(), DicomDate(DicomDateImpl::Month(1944, 2)) @@ -1064,12 +971,12 @@ mod tests { DicomTime(DicomTimeImpl::Hour(1)) ); // cheap precision checks - assert!( - DicomTime::from_hms_micro(9, 1, 1, 123456).unwrap().is_precise() - ); - assert!( - !DicomTime::from_hms_milli(9, 1, 1, 123).unwrap().is_precise() - ); + assert!(DicomTime::from_hms_micro(9, 1, 1, 123456) + .unwrap() + .is_precise()); + assert!(!DicomTime::from_hms_milli(9, 1, 1, 123) + .unwrap() + .is_precise()); assert_eq!( DicomTime::from_hms_milli(9, 1, 1, 123) @@ -1228,11 +1135,9 @@ mod tests { ); assert_eq!( - DicomDateTime::from_date( - DicomDate::from_ym(2020, 2).unwrap() - ) - .earliest() - .unwrap(), + DicomDateTime::from_date(DicomDate::from_ym(2020, 2).unwrap()) + .earliest() + .unwrap(), PreciseDateTimeResult::Naive(NaiveDateTime::new( NaiveDate::from_ymd_opt(2020, 2, 1).unwrap(), NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() @@ -1247,15 +1152,14 @@ mod tests { .latest() .unwrap(), PreciseDateTimeResult::TimeZone( - FixedOffset::east_opt(0) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(), - NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() - )) - .unwrap() + FixedOffset::east_opt(0) + .unwrap() + .from_local_datetime(&NaiveDateTime::new( + NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(), + NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() + )) + .unwrap() ) - ); assert_eq!( @@ -1268,15 +1172,14 @@ mod tests { .earliest() .unwrap(), PreciseDateTimeResult::TimeZone( - FixedOffset::east_opt(0) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(), - NaiveTime::from_hms_micro_opt(23, 59, 59, 100_000).unwrap() - )) - .unwrap() + FixedOffset::east_opt(0) + .unwrap() + .from_local_datetime(&NaiveDateTime::new( + NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(), + NaiveTime::from_hms_micro_opt(23, 59, 59, 100_000).unwrap() + )) + .unwrap() ) - ); assert_eq!( DicomDateTime::from_date_and_time_with_time_zone( @@ -1288,13 +1191,13 @@ mod tests { .latest() .unwrap(), PreciseDateTimeResult::TimeZone( - FixedOffset::east_opt(0) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(), - NaiveTime::from_hms_micro_opt(23, 59, 59, 109_999).unwrap() - )) - .unwrap() + FixedOffset::east_opt(0) + .unwrap() + .from_local_datetime(&NaiveDateTime::new( + NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(), + NaiveTime::from_hms_micro_opt(23, 59, 59, 109_999).unwrap() + )) + .unwrap() ) ); @@ -1401,17 +1304,17 @@ mod tests { DicomDateTime::from_date_and_time( DicomDate::from_ymd(2000, 1, 1).unwrap(), DicomTime::from_hms_milli(23, 59, 59, 10).unwrap() - ).unwrap() - .is_precise() == false - ); - - assert!( - DicomDateTime::from_date_and_time( - DicomDate::from_ymd(2000, 1, 1).unwrap(), - DicomTime::from_hms_micro(23, 59, 59, 654_321).unwrap() - ).unwrap() - .is_precise() + ) + .unwrap() + .is_precise() + == false ); + assert!(DicomDateTime::from_date_and_time( + DicomDate::from_ymd(2000, 1, 1).unwrap(), + DicomTime::from_hms_micro(23, 59, 59, 654_321).unwrap() + ) + .unwrap() + .is_precise()); } } diff --git a/core/src/value/primitive.rs b/core/src/value/primitive.rs index 1e9ff56b0..bfd0f9084 100644 --- a/core/src/value/primitive.rs +++ b/core/src/value/primitive.rs @@ -4,10 +4,9 @@ use super::DicomValueType; use crate::header::{HasLength, Length, Tag}; -use crate::value::partial::{DateComponent, DicomDate, DicomDateTime, DicomTime, Precision}; +use crate::value::partial::{DateComponent, DicomDate, DicomDateTime, DicomTime}; use crate::value::person_name::PersonName; use crate::value::range::{DateRange, DateTimeRange, TimeRange}; -use chrono::FixedOffset; use itertools::Itertools; use num_traits::NumCast; use safe_transmute::to_bytes::transmute_to_bytes; @@ -183,7 +182,7 @@ impl std::error::Error for ConvertValueError { pub type Result = std::result::Result; // Re-exported from chrono -pub use chrono::{DateTime, NaiveDate, NaiveTime}; +pub use chrono::{NaiveDate, NaiveTime}; /// An aggregation of one or more elements in a value. pub type C = SmallVec<[T; 2]>; @@ -2641,217 +2640,10 @@ impl PrimitiveValue { } } - /// Retrieve a single `chrono::DateTime` from this value. - /// - /// If the value is already represented as a precise `DicomDateTime`, - /// it is converted to `chrono::DateTime`. Imprecise values fail. - /// If the value is a string or sequence of strings, - /// the first string is decoded to obtain a date-time, - /// potentially failing if the string does not represent a valid time. - /// If the value in its textual form does not present a time zone, - /// `default_offset` is used. - /// If the value is a sequence of U8 bytes, the bytes are - /// first interpreted as an ASCII character string. - /// Otherwise, the operation fails. - /// - /// Users of this method are advised to retrieve - /// the default time zone offset - /// from the same source of the DICOM value. - /// - /// Users are advised that this method requires at least 1 out of 6 digits of the second - /// fraction .F to be present. Otherwise, the operation fails. - /// - /// Partial precision date-times are handled by `DicomDateTime`, - /// which can be retrieved by [`.to_datetime()`](PrimitiveValue::to_datetime). - /// - /// # Example - /// - /// ``` - /// # use dicom_core::value::{C, PrimitiveValue, DicomDateTime, DicomDate, DicomTime}; - /// # use smallvec::smallvec; - /// # use chrono::{DateTime, FixedOffset, TimeZone}; - /// # use std::error::Error; - /// # fn main() -> Result<(), Box> { - /// let default_offset = FixedOffset::east(0); - /// - /// // full accuracy `DicomDateTime` with a time-zone can be converted - /// assert_eq!( - /// PrimitiveValue::from( - /// DicomDateTime::from_date_and_time_with_time_zone( - /// DicomDate::from_ymd(2012, 12, 21)?, - /// DicomTime::from_hms_micro(9, 30, 1, 1)?, - /// default_offset - /// )? - /// ).to_chrono_datetime(default_offset)?, - /// FixedOffset::east(0) - /// .ymd(2012, 12, 21) - /// .and_hms_micro(9, 30, 1, 1) - /// , - /// ); - /// - /// assert_eq!( - /// PrimitiveValue::from("20121221093001.1") - /// .to_chrono_datetime(default_offset).ok(), - /// Some(FixedOffset::east(0) - /// .ymd(2012, 12, 21) - /// .and_hms_micro(9, 30, 1, 100_000) - /// ), - /// ); - /// # Ok(()) - /// # } - /// ``` - #[deprecated( - note = "As time-zone aware an naive DicomDateTime was introduced, this method could only access the time-zone - aware values, which would lead to confusing behavior." - )] - pub fn to_chrono_datetime( - &self, - default_offset: FixedOffset, - ) -> Result, ConvertValueError> { - match self { - PrimitiveValue::Str(s) => { - super::deserialize::parse_datetime(s.trim_end().as_bytes(), default_offset) - .context(ParseDateTimeSnafu) - .map_err(|err| ConvertValueError { - requested: "DateTime", - original: self.value_type(), - cause: Some(err), - }) - } - PrimitiveValue::Strs(s) => super::deserialize::parse_datetime( - s.first().map(|s| s.trim_end().as_bytes()).unwrap_or(&[]), - default_offset, - ) - .context(ParseDateTimeSnafu) - .map_err(|err| ConvertValueError { - requested: "DateTime", - original: self.value_type(), - cause: Some(err), - }), - PrimitiveValue::U8(bytes) => { - super::deserialize::parse_datetime(trim_last_whitespace(bytes), default_offset) - .context(ParseDateTimeSnafu) - .map_err(|err| ConvertValueError { - requested: "DateTime", - original: self.value_type(), - cause: Some(err), - }) - } - _ => Err(ConvertValueError { - requested: "DateTime", - original: self.value_type(), - cause: None, - }), - } - } - - /// Retrieve the full sequence of `chrono::DateTime`s from this value. - /// - /// If the value is already represented as a sequence of precise `DicomDateTime` values, - /// it is converted to a sequence of `chrono::DateTime` values. Imprecise values fail. - /// If the value is a string or sequence of strings, - /// the strings are decoded to obtain a date, potentially failing if - /// any of the strings does not represent a valid date. - /// If the value is a sequence of U8 bytes, the bytes are - /// first interpreted as an ASCII character string, - /// then as a backslash-separated list of date-times. - /// Otherwise, the operation fails. - /// - /// Users are advised that this method requires at least 1 out of 6 digits of the second - /// fraction .F to be present. Otherwise, the operation fails. - /// - /// Partial precision date-times are handled by `DicomDateTime`, - /// which can be retrieved by [`.to_multi_datetime()`](PrimitiveValue::to_multi_datetime). - /// - /// # Example - /// - /// ``` - /// # use dicom_core::value::{C, PrimitiveValue, DicomDate, DicomTime, DicomDateTime}; - /// # use smallvec::smallvec; - /// # use chrono::{FixedOffset, TimeZone}; - /// # fn main() -> Result<(), Box> { - /// let default_offset = FixedOffset::east(0); - /// - /// // full accuracy `DicomDateTime` can be converted - /// assert_eq!( - /// PrimitiveValue::from( - /// DicomDateTime::from_date_and_time( - /// DicomDate::from_ymd(2012, 12, 21)?, - /// DicomTime::from_hms_micro(9, 30, 1, 123_456)?, - /// default_offset - /// )? - /// ).to_multi_chrono_datetime(default_offset)?, - /// vec![FixedOffset::east(0) - /// .ymd(2012, 12, 21) - /// .and_hms_micro(9, 30, 1, 123_456) - /// ], - /// ); - /// - /// assert_eq!( - /// PrimitiveValue::Strs(smallvec![ - /// "20121221093001.123".to_string(), - /// "20180102100123.123456".to_string(), - /// ]).to_multi_chrono_datetime(default_offset).ok(), - /// Some(vec![ - /// FixedOffset::east(0) - /// .ymd(2012, 12, 21) - /// .and_hms_micro(9, 30, 1, 123_000), - /// FixedOffset::east(0) - /// .ymd(2018, 1, 2) - /// .and_hms_micro(10, 1, 23, 123_456) - /// ]), - /// ); - /// # Ok(()) - /// # } - /// ``` - #[deprecated( - note = "As time-zone aware an naive DicomDateTime was introduced, this method could only access the time-zone - aware values, which would lead to confusing behavior." - )] - pub fn to_multi_chrono_datetime( - &self, - default_offset: FixedOffset, - ) -> Result>, ConvertValueError> { - match self { - PrimitiveValue::Str(s) => { - super::deserialize::parse_datetime(s.trim_end().as_bytes(), default_offset) - .map(|date| vec![date]) - .context(ParseDateSnafu) - .map_err(|err| ConvertValueError { - requested: "DateTime", - original: self.value_type(), - cause: Some(err), - }) - } - PrimitiveValue::Strs(s) => s - .into_iter() - .map(|s| { - super::deserialize::parse_datetime(s.trim_end().as_bytes(), default_offset) - }) - .collect::, _>>() - .context(ParseDateSnafu) - .map_err(|err| ConvertValueError { - requested: "DateTime", - original: self.value_type(), - cause: Some(err), - }), - PrimitiveValue::U8(bytes) => trim_last_whitespace(bytes) - .split(|c| *c == b'\\') - .map(|s| super::deserialize::parse_datetime(s, default_offset)) - .collect::, _>>() - .context(ParseDateSnafu) - .map_err(|err| ConvertValueError { - requested: "DateTime", - original: self.value_type(), - cause: Some(err), - }), - _ => Err(ConvertValueError { - requested: "DateTime", - original: self.value_type(), - cause: None, - }), - } - } + #[deprecated(since = "0.6.4", note = "Use `to_datetime` instead")] + pub fn to_chrono_datetime(&self) {} + #[deprecated(since = "0.6.4", note = "Use `to_multi_datetime` instead")] + pub fn to_multi_chrono_datetime(&self) {} /// Retrieve a single `DicomDateTime` from this value. /// @@ -2878,7 +2670,7 @@ impl PrimitiveValue { /// use dicom_core::value::{DicomDateTime, AsRange, DateTimeRange, PreciseDateTimeResult}; /// /// # fn main() -> Result<(), Box> { - /// + /// /// // let's parse a date-time text value with 0.1 second precision without a time-zone. /// let dt_value = PrimitiveValue::from("20121221093001.1").to_datetime()?; /// @@ -2897,7 +2689,7 @@ impl PrimitiveValue { /// )) /// ); /// - /// let default_offset = FixedOffset::east(3600); + /// let default_offset = FixedOffset::east_opt(3600).unwrap(); /// // let's parse a date-time text value with full precision with a time-zone east +01:00. /// let dt_value = PrimitiveValue::from("20121221093001.123456+0100").to_datetime()?; /// @@ -2906,23 +2698,25 @@ impl PrimitiveValue { /// /// assert_eq!( /// dt_value.exact()?, - /// PreciseDateTimeResult::WithTimeZone( + /// PreciseDateTimeResult::TimeZone( /// default_offset - /// .ymd(2012,12,21) - /// .and_hms_micro(9, 30, 1, 123_456) - /// ) + /// .ymd_opt(2012, 12, 21).unwrap() + /// .and_hms_micro_opt(9, 30, 1, 123_456).unwrap() + /// ) + /// /// ); /// /// // ranges are inclusive, for a precise value, two identical values are returned /// assert_eq!( /// dt_value.range()?, /// DateTimeRange::from_start_to_end_with_time_zone( - /// FixedOffset::east(3600) - /// .ymd(2012, 12, 21) - /// .and_hms_micro(9, 30, 1, 123_456), - /// FixedOffset::east(3600) - /// .ymd(2012, 12, 21) - /// .and_hms_micro(9, 30, 1, 123_456))? + /// FixedOffset::east_opt(3600).unwrap() + /// .ymd_opt(2012, 12, 21).unwrap() + /// .and_hms_micro_opt(9, 30, 1, 123_456).unwrap(), + /// FixedOffset::east_opt(3600).unwrap() + /// .ymd_opt(2012, 12, 21).unwrap() + /// .and_hms_micro_opt(9, 30, 1, 123_456).unwrap() + /// )? /// /// ); /// # Ok(()) @@ -2993,7 +2787,7 @@ impl PrimitiveValue { }), PrimitiveValue::U8(bytes) => trim_last_whitespace(bytes) .split(|c| *c == b'\\') - .map(|s| super::deserialize::parse_datetime_partial(s)) + .map(super::deserialize::parse_datetime_partial) .collect::, _>>() .context(ParseDateSnafu) .map_err(|err| ConvertValueError { @@ -3168,7 +2962,7 @@ impl PrimitiveValue { /// /// ``` /// # use dicom_core::value::{C, PrimitiveValue}; - /// use chrono::{DateTime, FixedOffset, TimeZone}; + /// use chrono::{DateTime, NaiveDate, NaiveTime, NaiveDateTime, FixedOffset, TimeZone}; /// # use std::error::Error; /// use dicom_core::value::{DateTimeRange, PreciseDateTimeResult}; /// @@ -3176,35 +2970,61 @@ impl PrimitiveValue { /// /// // let's parse a text representation of a date-time range, where the lower bound is a microsecond /// // precision value with a time-zone (east +05:00) and the upper bound is a minimum precision value - /// // without a specified time-zone - /// let dt_range = PrimitiveValue::from("19920101153020.123+0500-1993").to_datetime_range()?; + /// // with a time-zone + /// let dt_range = PrimitiveValue::from("19920101153020.123+0500-1993+0300").to_datetime_range()?; /// - /// // lower bound of range is parsed into a PreciseDateTimeResult::WithTimeZone variant + /// // lower bound of range is parsed into a PreciseDateTimeResult::TimeZone variant /// assert_eq!( /// dt_range.start(), - /// Some(PreciseDateTimeResult::WithTimeZone( - /// FixedOffset::east_opt(5*3600).unwrap().ymd(1992, 1, 1) - /// .and_hms_micro(15, 30, 20, 123_000) + /// Some(PreciseDateTimeResult::TimeZone( + /// FixedOffset::east_opt(5*3600).unwrap().ymd_opt(1992, 1, 1).unwrap() + /// .and_hms_micro_opt(15, 30, 20, 123_000).unwrap() /// ) /// ) /// ); /// - /// // null components of date-time default to latest possible - /// // because lower bound value is time-zone aware, the upper bound will also be parsed - /// // into a time-zone aware value with + /// // upper bound of range is parsed into a PreciseDateTimeResult::TimeZone variant /// assert_eq!( /// dt_range.end(), - /// Some(PreciseDateTimeResult::WithTimeZone( - /// FixedOffset::east_opt(0).unwrap().ymd(1993, 12, 31) - /// .and_hms_micro(23, 59, 59, 999_999) + /// Some(PreciseDateTimeResult::TimeZone( + /// FixedOffset::east_opt(3*3600).unwrap().ymd_opt(1993, 12, 31).unwrap() + /// .and_hms_micro_opt(23, 59, 59, 999_999).unwrap() /// ) /// ) /// ); /// - /// let range_from = PrimitiveValue::from("2012-").to_datetime_range(offset)?; + /// + /// + /// let range_from = PrimitiveValue::from("2012-").to_datetime_range()?; /// /// assert!(range_from.end().is_none()); /// + /// // The DICOM protocol allows for parsing text representations of date-time ranges, + /// // where one bound has a time-zone but the other has not. + /// let dt_range = PrimitiveValue::from("1992+0500-1993").to_datetime_range()?; + /// + /// // the resulting DateTimeRange variant holds and ambiguous value, that needs further interpretation. + /// assert_eq!( + /// + /// dt_range, + /// DateTimeRange::AmbiguousEnd{ + /// start: FixedOffset::east_opt(5*3600).unwrap().ymd_opt(1992, 1, 1).unwrap() + /// .and_hms_micro_opt(0, 0, 0, 0).unwrap(), + /// // this naive value needs further interpretation + /// ambiguous_end: NaiveDateTime::new( + /// NaiveDate::from_ymd_opt(1993, 12, 31).unwrap(), + /// NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap(), + /// ) + /// + /// } + /// + /// ); + /// + /// // ambiguous date-time time range holds a precise lower bound + /// assert!(dt_range.start().is_some()); + /// // but not a precise upper bound + /// assert!(dt_range.end().is_none()); + /// /// # Ok(()) /// # } /// ``` @@ -4834,91 +4654,6 @@ mod tests { )); } - #[test] - fn primitive_value_to_chrono_datetime() { - let this_datetime = FixedOffset::east_opt(1) - .unwrap() - .with_ymd_and_hms(2012, 12, 21, 11, 9, 26) - .unwrap(); - let this_datetime_frac = FixedOffset::east_opt(1) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2012, 12, 21).unwrap(), - NaiveTime::from_hms_micro_opt(11, 9, 26, 380_000).unwrap(), - )) - .unwrap(); - - // from text (Str) - fraction is mandatory even if zero - assert_eq!( - dicom_value!(Str, "20121221110926.0") - .to_chrono_datetime(FixedOffset::east_opt(1).unwrap()) - .unwrap(), - this_datetime, - ); - // from text with fraction of a second + padding - assert_eq!( - PrimitiveValue::from("20121221110926.38 ") - .to_chrono_datetime(FixedOffset::east_opt(1).unwrap()) - .unwrap(), - this_datetime_frac, - ); - // from text (Strs) - fraction is mandatory even if zero - assert_eq!( - dicom_value!(Strs, ["20121221110926.0"]) - .to_chrono_datetime(FixedOffset::east_opt(1).unwrap()) - .unwrap(), - this_datetime, - ); - // from text (Strs) with fraction of a second + padding - assert_eq!( - dicom_value!(Strs, ["20121221110926.38 "]) - .to_chrono_datetime(FixedOffset::east_opt(1).unwrap()) - .unwrap(), - this_datetime_frac, - ); - // from bytes with fraction of a second + padding - assert_eq!( - PrimitiveValue::from(&b"20121221110926.38 "[..]) - .to_chrono_datetime(FixedOffset::east_opt(1).unwrap()) - .unwrap(), - this_datetime_frac, - ); - - // without fraction of a second - let this_datetime = FixedOffset::east_opt(1) - .unwrap() - .with_ymd_and_hms(2012, 12, 21, 11, 9, 26) - .unwrap(); - assert_eq!( - dicom_value!(Str, "20121221110926") - .to_chrono_datetime(FixedOffset::east_opt(1).unwrap()) - .unwrap(), - this_datetime, - ); - - // without seconds - assert!(matches!( - PrimitiveValue::from("201212211109") - .to_chrono_datetime(FixedOffset::east_opt(1).unwrap()), - Err(ConvertValueError { - requested: "DateTime", - original: ValueType::Str, - .. - }) - )); - - // not a datetime - assert!(matches!( - PrimitiveValue::from("Smith^John") - .to_chrono_datetime(FixedOffset::east_opt(1).unwrap()), - Err(ConvertValueError { - requested: "DateTime", - original: ValueType::Str, - .. - }) - )); - } - #[test] fn primitive_value_to_dicom_datetime() { let offset = FixedOffset::east_opt(1).unwrap(); @@ -5051,7 +4786,6 @@ mod tests { #[test] fn primitive_value_to_datetime_range() { - assert_eq!( dicom_value!(Str, "202002-20210228153012.123") .to_datetime_range() @@ -5073,7 +4807,7 @@ mod tests { PrimitiveValue::from(&b"2020-2030+0800"[..]) .to_datetime_range() .unwrap(), - DateTimeRange::AmbiguousStart { + DateTimeRange::AmbiguousStart { ambiguous_start: NaiveDateTime::new( NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(), NaiveTime::from_hms_opt(0, 0, 0).unwrap() @@ -5085,7 +4819,7 @@ mod tests { NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() )) .unwrap() - } + } ); } diff --git a/core/src/value/range.rs b/core/src/value/range.rs index 8ec3e0cc9..8c979644b 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -1,15 +1,13 @@ //! Handling of date, time, date-time ranges. Needed for range matching. //! Parsing into ranges happens via partial precision structures (DicomDate, DicomTime, //! DicomDatime) so ranges can handle null components in date, time, date-time values. -use chrono::{ - DateTime, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, -}; +use chrono::{DateTime, FixedOffset, LocalResult, 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::{DateComponent, DicomDate, DicomDateTime, DicomTime}; +use crate::value::partial::{DicomDate, DicomDateTime, DicomTime}; #[derive(Debug, Snafu)] #[non_exhaustive] @@ -90,11 +88,11 @@ type Result = std::result::Result; /// Imprecise values are to be handled as date / time ranges. /// /// This trait is implemented by date / time structures with partial precision. -/// -/// [Precision::is_precise()] method will check if the given value has full precision. If so, it can be -/// converted with [AsRange::exact()] to a `chrono` value. If not, [AsRange::range()] will yield a -/// date / time range. -/// +/// +/// [AsRange::is_precise()] method will check if the given value has full precision. If so, it can be +/// converted with [AsRange::exact()] to a precise value. If not, [AsRange::range()] will yield a +/// date / time / date-time range. +/// /// Please note that precision does not equal validity. A precise 'YYYYMMDD' [DicomDate] can still /// fail to produce a valid [chrono::NaiveDate] /// @@ -105,7 +103,7 @@ type Result = std::result::Result; /// # use smallvec::smallvec; /// # use std::error::Error; /// use chrono::{NaiveDate, NaiveTime}; -/// use dicom_core::value::{AsRange, DicomDate, DicomTime, TimeRange}; +/// use dicom_core::value::{AsRange, DicomDate, DicomTime, DateRange, TimeRange}; /// # fn main() -> Result<(), Box> { /// /// let dicom_date = DicomDate::from_ym(2010,1)?; @@ -127,19 +125,19 @@ type Result = std::result::Result; /// ); /// // only a time with 6 digits second fraction is considered precise /// assert!(dicom_time.exact().is_err()); -/// +/// /// let primitive = PrimitiveValue::from("199402"); -/// +/// /// // This is the fastest way to get to a useful value, but it fails not only for invalid -/// // dates but for imprecise ones as well. +/// // dates but for imprecise ones as well. /// assert!(primitive.to_naive_date().is_err()); -/// +/// /// // We should take indermediary steps ... -/// +/// /// // The parser now checks for basic year and month value ranges here. /// // But, it would not detect invalid dates like 30th of february etc. /// let dicom_date : DicomDate = primitive.to_date()?; -/// +/// /// // now we have a valid DicomDate value, let's check if it's precise. /// if dicom_date.is_precise(){ /// // no components are missing, we can proceed by calling .exact() @@ -147,19 +145,19 @@ type Result = std::result::Result; /// let precise_date: NaiveDate = dicom_date.exact()?; /// } /// else{ -/// // day and / or month are missing, no need to call expensive .exact() method -/// // - it will fail +/// // day and / or month are missing, no need to call expensive .exact() method +/// // - it will fail /// // try to retrieve the date range instead /// let date_range: DateRange = dicom_date.range()?; -/// +/// /// // the real conversion to a `chrono` value only happens at this stage /// if let Some(start) = date_range.start(){ /// // the range has a given lower date bound -/// } -/// +/// } +/// /// // or try to retrieve the earliest possible value directly from DicomDate /// let earliest: NaiveDate = dicom_date.earliest()?; -/// +/// /// } /// /// # Ok(()) @@ -192,7 +190,6 @@ pub trait AsRange { /// Returns a tuple of the earliest and latest possible value from a partial precision structure. fn range(&self) -> Result; - } impl AsRange for DicomDate { @@ -271,10 +268,7 @@ impl AsRange for DicomTime { type Range = TimeRange; fn is_precise(&self) -> bool { - match self.fraction_and_precision(){ - Some((fr_, precision)) if precision == &6 => true, - _ => false - } + matches!(self.fraction_and_precision(), Some((_fr_, precision)) if precision == &6) } fn earliest(&self) -> Result { @@ -330,9 +324,9 @@ impl AsRange for DicomDateTime { type Range = DateTimeRange; fn is_precise(&self) -> bool { - match self.time(){ + match self.time() { Some(dicom_time) => dicom_time.is_precise(), - None => false + None => false, } } @@ -426,8 +420,7 @@ impl DicomTime { pub fn to_naive_time(self) -> Result { if self.second().is_some() { self.earliest() - } - else{ + } else { ImpreciseValueSnafu.fail() } } @@ -466,6 +459,14 @@ impl DicomDateTime { NoTimeZoneSnafu.fail() } } + + #[deprecated( + since = "0.6.4", + note = "Use `to_datetime_with_time_zone` or `to_naive_date_time`" + )] + pub fn to_chrono_datetime(self) -> Result> { + DateTimeInvalidSnafu.fail() + } } /// Represents a date range as two [`Option`] values. @@ -505,9 +506,9 @@ pub struct TimeRange { /// Represents a date-time range, that can either be time-zone naive or time-zone aware. It is stored as two [`Option>`] or /// two [`Option>`] values. /// [None] means no upper or no lower bound for range is present. -/// -/// In borderline cases the parser produces [DateTimeRange::AmbiguousStart] and [DateTimeRange::AmbiguousEnd]. -/// +/// +/// In borderline cases the parser produces [DateTimeRange::AmbiguousStart] and [DateTimeRange::AmbiguousEnd]. +/// /// # Example /// ``` /// # use std::error::Error; @@ -517,7 +518,7 @@ pub struct TimeRange { /// /// let offset = FixedOffset::west_opt(3600).unwrap(); /// -/// let dtr = DateTimeRange::from_start_to_end( +/// let dtr = DateTimeRange::from_start_to_end_with_time_zone( /// offset.from_local_datetime(&NaiveDateTime::new( /// NaiveDate::from_ymd_opt(2000, 5, 6).unwrap(), /// NaiveTime::from_hms_opt(15, 0, 0).unwrap() @@ -535,7 +536,7 @@ pub struct TimeRange { /// ``` #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] pub enum DateTimeRange { - /// DateTime range without time-zone information + /// DateTime range without time-zone information Naive { start: Option, end: Option, @@ -549,30 +550,30 @@ pub enum DateTimeRange { /// This variant can only be a result of parsing a date-time range from an external source. /// The standard allows for parsing a date-time range in which one DT value provides time-zone /// information but the other does not. - /// + /// /// Example '19750101-19800101+0200'. - /// + /// /// In such case, the missing time-zone can be interpreted as the local time-zone or the time-zone - /// provided with the upper bound (or something else altogether). + /// provided with the upper bound (or something else altogether). /// And that is why this variant is not guaranteed to be monotonically ordered in time. - AmbiguousStart{ + AmbiguousStart { ambiguous_start: NaiveDateTime, - end: DateTime + end: DateTime, }, /// DateTime range with a upper bound value which is ambiguous and needs further interpretation. /// This variant can only be a result of parsing a date-time range from an external source. /// The standard allows for parsing a date-time range in which one DT value provides time-zone /// information but the other does not. - /// + /// /// Example '19750101+0200-19800101'. - /// + /// /// In such case, the missing time-zone can be interpreted as the local time-zone or the time-zone /// provided with the lower bound (or something else altogether). /// And that is why this variant is not guaranteed to be monotonically ordered in time. - AmbiguousEnd{ + AmbiguousEnd { start: DateTime, - ambiguous_end: NaiveDateTime - } + ambiguous_end: NaiveDateTime, + }, } /// A precise date-time value, that can either be time-zone aware or time-zone naive. @@ -790,33 +791,32 @@ impl DateTimeRange { } /// Returns the lower bound of the range. - /// + /// /// If the date-time range contains an ambiguous lower bound value, it will return `None`. /// See [DateTimeRange::AmbiguousStart] pub fn start(&self) -> Option { match self { - DateTimeRange::Naive { start, .. } => start.map(|dt| PreciseDateTimeResult::Naive(dt)), - DateTimeRange::TimeZone { start, .. } => { - start.map(|dt| PreciseDateTimeResult::TimeZone(dt)) - } + DateTimeRange::Naive { start, .. } => start.map(PreciseDateTimeResult::Naive), + DateTimeRange::TimeZone { start, .. } => start.map(PreciseDateTimeResult::TimeZone), DateTimeRange::AmbiguousStart { .. } => None, - DateTimeRange::AmbiguousEnd { start, .. } => Some(PreciseDateTimeResult::TimeZone(start.clone())), - + DateTimeRange::AmbiguousEnd { start, .. } => { + Some(PreciseDateTimeResult::TimeZone(*start)) + } } - } /// Returns the upper bound of the range. - /// + /// /// If the date-time range contains an ambiguous upper bound value, it will return `None`. /// See [DateTimeRange::AmbiguousEnd] pub fn end(&self) -> Option { match self { - DateTimeRange::Naive { start: _, end } => end.map(|dt| PreciseDateTimeResult::Naive(dt)), - DateTimeRange::TimeZone { start: _, end } => { - end.map(|dt| PreciseDateTimeResult::TimeZone(dt)) - }, - DateTimeRange::AmbiguousStart { ambiguous_start: _, end } => Some(PreciseDateTimeResult::TimeZone(end.clone())), + DateTimeRange::Naive { start: _, end } => end.map(PreciseDateTimeResult::Naive), + DateTimeRange::TimeZone { start: _, end } => end.map(PreciseDateTimeResult::TimeZone), + DateTimeRange::AmbiguousStart { + ambiguous_start: _, + end, + } => Some(PreciseDateTimeResult::TimeZone(*end)), DateTimeRange::AmbiguousEnd { .. } => None, } } @@ -863,11 +863,12 @@ impl DateTimeRange { }, } } - + /// Returns true, if one value in the range is ambiguous. + /// + /// For details see [DateTimeRange::AmbiguousEnd] or [DateTimeRange::AmbiguousStart] pub fn is_ambiguous(&self) -> bool { - match self{ - DateTimeRange::AmbiguousEnd { .. } | &DateTimeRange::AmbiguousStart { .. } => true, - _ => false + matches! {self, + &DateTimeRange::AmbiguousEnd { .. } | &DateTimeRange::AmbiguousStart { .. } } } } @@ -1017,16 +1018,18 @@ pub fn parse_datetime_range(buf: &[u8]) -> Result { // lower bound time-zone was missing PreciseDateTimeResult::Naive(start), PreciseDateTimeResult::TimeZone(end), - ) => { - Ok(DateTimeRange::AmbiguousStart { ambiguous_start: start, end }) - } + ) => Ok(DateTimeRange::AmbiguousStart { + ambiguous_start: start, + end, + }), ( PreciseDateTimeResult::TimeZone(start), // upper bound time-zone was missing PreciseDateTimeResult::Naive(end), - ) => { - Ok(DateTimeRange::AmbiguousEnd { start, ambiguous_end: end }) - } + ) => Ok(DateTimeRange::AmbiguousEnd { + start, + ambiguous_end: end, + }), }; match dtr { Ok(val) => return Ok(val), @@ -1052,17 +1055,22 @@ pub fn parse_datetime_range(buf: &[u8]) -> Result { (PreciseDateTimeResult::Naive(start), PreciseDateTimeResult::Naive(end)) => { DateTimeRange::from_start_to_end(start, end) } - ( - PreciseDateTimeResult::TimeZone(start), - PreciseDateTimeResult::TimeZone(end), - ) => DateTimeRange::from_start_to_end_with_time_zone(start, end), + (PreciseDateTimeResult::TimeZone(start), PreciseDateTimeResult::TimeZone(end)) => { + DateTimeRange::from_start_to_end_with_time_zone(start, end) + } // lower bound time-zone was missing (PreciseDateTimeResult::Naive(start), PreciseDateTimeResult::TimeZone(end)) => { - Ok(DateTimeRange::AmbiguousStart { ambiguous_start: start, end }) + Ok(DateTimeRange::AmbiguousStart { + ambiguous_start: start, + end, + }) } // upper bound time-zone was missing (PreciseDateTimeResult::TimeZone(start), PreciseDateTimeResult::Naive(end)) => { - Ok(DateTimeRange::AmbiguousEnd { start, ambiguous_end: end }) + Ok(DateTimeRange::AmbiguousEnd { + start, + ambiguous_end: end, + }) } } } @@ -1169,8 +1177,7 @@ mod tests { NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap() )) .unwrap() - ) - ) + )) ); assert_eq!( DateTimeRange::from_end_with_time_zone( @@ -1189,8 +1196,7 @@ mod tests { NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap() )) .unwrap() - ) - ) + )) ); assert_eq!( DateTimeRange::from_start_to_end_with_time_zone( @@ -1270,78 +1276,62 @@ mod tests { #[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() - ) - - ) + 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(PreciseDateTimeResult::Naive( - NaiveDateTime::new( - NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), - NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap() - )) - - - ) + Some(PreciseDateTimeResult::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() - ) - ) + 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(PreciseDateTimeResult::Naive( - NaiveDateTime::new( - NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), - NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap() - ) - ) - ) + Some(PreciseDateTimeResult::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() - ), + 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() - ) + NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), + NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap() + ) ) .unwrap() .start(), - Some(PreciseDateTimeResult::Naive( - NaiveDateTime::new( - NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), - NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap() - ) - )) + Some(PreciseDateTimeResult::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() - ), + 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() - ) + NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), + NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap() + ) ) .unwrap() .end(), - Some(PreciseDateTimeResult::Naive( - NaiveDateTime::new( - NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), - NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap() - ) - )) + Some(PreciseDateTimeResult::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( @@ -1362,7 +1352,6 @@ mod tests { )); } - #[test] fn test_parse_date_range() { assert_eq!( @@ -1474,88 +1463,73 @@ mod tests { #[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() - ) - ) + 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() - ) - ) + 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() - ) - ) + 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() - ) - ) + 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() - ) - ) + 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() - ) - ), + 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() - ) - ), + start: Some(NaiveDateTime::new( + NaiveDate::from_ymd_opt(2, 12, 31).unwrap(), + NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() + )), end: None }) ); @@ -1632,25 +1606,21 @@ mod tests { }) ); // one 'west' Time zone offset gets parsed, offset cannot be mistaken for a date-time - // the missing Time zone offset will result in an ambiguous range variant + // the missing Time zone offset will result in an ambiguous range variant assert_eq!( parse_datetime_range(b"19900101-1200-1999").unwrap(), - DateTimeRange::AmbiguousEnd { - start: - 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() - , - ambiguous_end: - NaiveDateTime::new( - NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(), - NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() - ) - + DateTimeRange::AmbiguousEnd { + start: 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(), + ambiguous_end: NaiveDateTime::new( + NaiveDate::from_ymd_opt(1999, 12, 31).unwrap(), + NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() + ) } ); // '0500' can either be a valid west UTC offset on the lower bound, or a valid date-time on the upper bound @@ -1658,22 +1628,18 @@ mod tests { // and will result in an ambiguous range variant. assert_eq!( parse_datetime_range(b"0050-0500-1000").unwrap(), - DateTimeRange::AmbiguousStart { - ambiguous_start: - NaiveDateTime::new( - NaiveDate::from_ymd_opt(50, 1, 1).unwrap(), - NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() - ) - , - end: - 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() - + DateTimeRange::AmbiguousStart { + ambiguous_start: NaiveDateTime::new( + NaiveDate::from_ymd_opt(50, 1, 1).unwrap(), + NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() + ), + end: 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() } ); // sequence with more than 3 dashes '-' is refused. diff --git a/parser/src/stateful/decode.rs b/parser/src/stateful/decode.rs index d37225072..9096c5e9c 100644 --- a/parser/src/stateful/decode.rs +++ b/parser/src/stateful/decode.rs @@ -2,7 +2,6 @@ //! which also supports text decoding. use crate::util::n_times; -use chrono::FixedOffset; use dicom_core::header::{DataElementHeader, HasLength, Length, SequenceItemHeader, Tag, VR}; use dicom_core::value::deserialize::{ parse_date_partial, parse_datetime_partial, parse_time_partial, From 8ee3357d878653c97dcff4879e27da9f4e0c77d3 Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Sat, 10 Feb 2024 23:05:23 +0100 Subject: [PATCH 08/23] - simplify DateTimeRange - default handling of ambiguous DT Ranges - needs chrono 'clock' feature --- Cargo.lock | 125 +++++++++++++++ core/Cargo.toml | 5 +- core/src/value/primitive.rs | 157 +++++++++++++++---- core/src/value/range.rs | 303 ++++++++++++++++++++++++++---------- 4 files changed, 470 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de36e3e3e..fb68e237b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.4" @@ -134,6 +149,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + [[package]] name = "bytemuck" version = "1.14.0" @@ -196,7 +217,10 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ + "android-tzdata", + "iana-time-zone", "num-traits", + "windows-targets 0.48.5", ] [[package]] @@ -273,6 +297,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "cpufeatures" version = "0.2.11" @@ -984,6 +1014,29 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.4.0" @@ -1115,6 +1168,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2037,6 +2099,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + [[package]] name = "webpki-roots" version = "0.25.2" @@ -2080,6 +2196,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index e93f80174..3c701678c 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,12 +10,9 @@ keywords = ["dicom"] readme = "README.md" [dependencies] -chrono = { version = "0.4.31", default-features = false, features = ["std"] } +chrono = { version = "0.4.31", default-features = false, features = ["std", "clock"] } itertools = "0.12" num-traits = "0.2.12" safe-transmute = "0.11.0" smallvec = "1.6.1" snafu = "0.7.3" - -#[lib] -#doctest = false \ No newline at end of file diff --git a/core/src/value/primitive.rs b/core/src/value/primitive.rs index bfd0f9084..126c10a20 100644 --- a/core/src/value/primitive.rs +++ b/core/src/value/primitive.rs @@ -6,7 +6,7 @@ use super::DicomValueType; use crate::header::{HasLength, Length, Tag}; use crate::value::partial::{DateComponent, DicomDate, DicomDateTime, DicomTime}; use crate::value::person_name::PersonName; -use crate::value::range::{DateRange, DateTimeRange, TimeRange}; +use crate::value::range::{AmbiguousDtRangeParser, DateRange, DateTimeRange, TimeRange}; use itertools::Itertools; use num_traits::NumCast; use safe_transmute::to_bytes::transmute_to_bytes; @@ -2993,38 +2993,30 @@ impl PrimitiveValue { /// ) /// ); /// + /// let lower = PrimitiveValue::from("2012-").to_datetime_range()?; /// - /// - /// let range_from = PrimitiveValue::from("2012-").to_datetime_range()?; - /// - /// assert!(range_from.end().is_none()); + /// assert!(lower.end().is_none()); /// /// // The DICOM protocol allows for parsing text representations of date-time ranges, /// // where one bound has a time-zone but the other has not. /// let dt_range = PrimitiveValue::from("1992+0500-1993").to_datetime_range()?; /// - /// // the resulting DateTimeRange variant holds and ambiguous value, that needs further interpretation. + /// // the default behavior in this case is to use the known time-zone to construct + /// // two time-zone aware DT bounds. /// assert_eq!( - /// /// dt_range, - /// DateTimeRange::AmbiguousEnd{ - /// start: FixedOffset::east_opt(5*3600).unwrap().ymd_opt(1992, 1, 1).unwrap() - /// .and_hms_micro_opt(0, 0, 0, 0).unwrap(), - /// // this naive value needs further interpretation - /// ambiguous_end: NaiveDateTime::new( - /// NaiveDate::from_ymd_opt(1993, 12, 31).unwrap(), - /// NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap(), + /// DateTimeRange::TimeZone{ + /// start: Some(FixedOffset::east_opt(5*3600).unwrap() + /// .ymd_opt(1992, 1, 1).unwrap() + /// .and_hms_micro_opt(0, 0, 0, 0).unwrap() + /// ), + /// end: Some(FixedOffset::east_opt(5*3600).unwrap() + /// .ymd_opt(1993, 12, 31).unwrap() + /// .and_hms_micro_opt(23, 59, 59, 999_999).unwrap() /// ) - /// /// } - /// /// ); /// - /// // ambiguous date-time time range holds a precise lower bound - /// assert!(dt_range.start().is_some()); - /// // but not a precise upper bound - /// assert!(dt_range.end().is_none()); - /// /// # Ok(()) /// # } /// ``` @@ -3063,6 +3055,97 @@ impl PrimitiveValue { } } + /// Retrieve a single `DateTimeRange` from this value. + /// + /// Use a custom ambiguous date-time range parser. + /// + /// See [PrimitiveValue::to_datetime_range] and [AmbiguousDtRangeParser] + /// # Example + /// + /// ``` + /// # use dicom_core::value::{C, PrimitiveValue}; + /// # use std::error::Error; + /// use dicom_core::value::range::{AmbiguousDtRangeParser, ToLocalTimeZone, IgnoreTimeZone, DateTimeRange}; + /// use chrono::{NaiveDate, NaiveTime, NaiveDateTime}; + /// # fn main() -> Result<(), Box> { + /// + /// // The DICOM protocol allows for parsing text representations of date-time ranges, + /// // where one bound has a time-zone but the other has not. + /// // the default behavior in this case is to use the known time-zone to construct + /// // two time-zone aware DT bounds. But we want to use the local clock time-zone instead + /// let dt_range = PrimitiveValue::from("1992+0599-1993") + /// .to_datetime_range_custom::()?; + /// + /// // local clock time-zone in the upper bound should be different from 0599 in the lower bound. + /// assert_ne!( + /// dt_range.start().unwrap() + /// .as_datetime_with_time_zone().unwrap() + /// .offset(), + /// dt_range.end().unwrap() + /// .as_datetime_with_time_zone().unwrap() + /// .offset() + /// ); + /// + /// // ignores parsed time-zone, retrieve naive range + /// let naive_range = PrimitiveValue::from("1992+0599-1993") + /// .to_datetime_range_custom::()?; + /// + /// assert_eq!( + /// naive_range, + /// DateTimeRange::from_start_to_end( + /// NaiveDateTime::new( + /// NaiveDate::from_ymd_opt(1992, 1, 1).unwrap(), + /// NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() + /// ), + /// NaiveDateTime::new( + /// NaiveDate::from_ymd_opt(1993, 12, 31).unwrap(), + /// NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() + /// ) + /// ).unwrap() + /// ); + /// + /// # Ok(()) + /// # } + /// ``` + pub fn to_datetime_range_custom( + &self, + ) -> Result { + match self { + PrimitiveValue::Str(s) => { + super::range::parse_datetime_range_custom::(s.trim_end().as_bytes()) + .context(ParseDateTimeRangeSnafu) + .map_err(|err| ConvertValueError { + requested: "DateTimeRange", + original: self.value_type(), + cause: Some(err), + }) + } + PrimitiveValue::Strs(s) => super::range::parse_datetime_range_custom::( + s.first().map(|s| s.trim_end().as_bytes()).unwrap_or(&[]), + ) + .context(ParseDateTimeRangeSnafu) + .map_err(|err| ConvertValueError { + requested: "DateTimeRange", + original: self.value_type(), + cause: Some(err), + }), + PrimitiveValue::U8(bytes) => { + super::range::parse_datetime_range_custom::(trim_last_whitespace(bytes)) + .context(ParseDateTimeRangeSnafu) + .map_err(|err| ConvertValueError { + requested: "DateTimeRange", + original: self.value_type(), + cause: Some(err), + }) + } + _ => Err(ConvertValueError { + requested: "DateTimeRange", + original: self.value_type(), + cause: None, + }), + } + } + /// Retrieve a single [`PersonName`][1] from this value. /// /// If the value is a string or sequence of strings, @@ -4802,23 +4885,31 @@ mod tests { ) .unwrap() ); - // East UTC offset gets parsed but will result in an ambiguous dt-range variant + // East UTC offset gets parsed and the missing lower bound time-zone + // will be the same as the parsed offset value assert_eq!( PrimitiveValue::from(&b"2020-2030+0800"[..]) .to_datetime_range() .unwrap(), - DateTimeRange::AmbiguousStart { - ambiguous_start: NaiveDateTime::new( - NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(), - NaiveTime::from_hms_opt(0, 0, 0).unwrap() + DateTimeRange::TimeZone { + start: Some( + FixedOffset::east_opt(8 * 3600) + .unwrap() + .from_local_datetime(&NaiveDateTime::new( + NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(), + NaiveTime::from_hms_opt(0, 0, 0).unwrap() + )) + .unwrap() ), - end: FixedOffset::east_opt(8 * 3600) - .unwrap() - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2030, 12, 31).unwrap(), - NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() - )) - .unwrap() + end: Some( + FixedOffset::east_opt(8 * 3600) + .unwrap() + .from_local_datetime(&NaiveDateTime::new( + NaiveDate::from_ymd_opt(2030, 12, 31).unwrap(), + NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() + )) + .unwrap() + ) } ); } diff --git a/core/src/value/range.rs b/core/src/value/range.rs index 8c979644b..952631c2c 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -1,7 +1,9 @@ //! Handling of date, time, date-time ranges. Needed for range matching. //! Parsing into ranges happens via partial precision structures (DicomDate, DicomTime, //! DicomDatime) so ranges can handle null components in date, time, date-time values. -use chrono::{DateTime, FixedOffset, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone}; +use chrono::{ + DateTime, FixedOffset, Local, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, +}; use snafu::{Backtrace, OptionExt, ResultExt, Snafu}; use crate::value::deserialize::{ @@ -545,35 +547,7 @@ pub enum DateTimeRange { TimeZone { start: Option>, end: Option>, - }, - /// DateTime range with a lower bound value which is ambiguous and needs further interpretation. - /// This variant can only be a result of parsing a date-time range from an external source. - /// The standard allows for parsing a date-time range in which one DT value provides time-zone - /// information but the other does not. - /// - /// Example '19750101-19800101+0200'. - /// - /// In such case, the missing time-zone can be interpreted as the local time-zone or the time-zone - /// provided with the upper bound (or something else altogether). - /// And that is why this variant is not guaranteed to be monotonically ordered in time. - AmbiguousStart { - ambiguous_start: NaiveDateTime, - end: DateTime, - }, - /// DateTime range with a upper bound value which is ambiguous and needs further interpretation. - /// This variant can only be a result of parsing a date-time range from an external source. - /// The standard allows for parsing a date-time range in which one DT value provides time-zone - /// information but the other does not. - /// - /// Example '19750101+0200-19800101'. - /// - /// In such case, the missing time-zone can be interpreted as the local time-zone or the time-zone - /// provided with the lower bound (or something else altogether). - /// And that is why this variant is not guaranteed to be monotonically ordered in time. - AmbiguousEnd { - start: DateTime, - ambiguous_end: NaiveDateTime, - }, + } } /// A precise date-time value, that can either be time-zone aware or time-zone naive. @@ -790,34 +764,19 @@ impl DateTimeRange { } } - /// Returns the lower bound of the range. - /// - /// If the date-time range contains an ambiguous lower bound value, it will return `None`. - /// See [DateTimeRange::AmbiguousStart] + /// Returns the lower bound of the range, if present. pub fn start(&self) -> Option { match self { DateTimeRange::Naive { start, .. } => start.map(PreciseDateTimeResult::Naive), DateTimeRange::TimeZone { start, .. } => start.map(PreciseDateTimeResult::TimeZone), - DateTimeRange::AmbiguousStart { .. } => None, - DateTimeRange::AmbiguousEnd { start, .. } => { - Some(PreciseDateTimeResult::TimeZone(*start)) - } } } - /// Returns the upper bound of the range. - /// - /// If the date-time range contains an ambiguous upper bound value, it will return `None`. - /// See [DateTimeRange::AmbiguousEnd] + /// Returns the upper bound of the range, if present. pub fn end(&self) -> Option { match self { DateTimeRange::Naive { start: _, end } => end.map(PreciseDateTimeResult::Naive), DateTimeRange::TimeZone { start: _, end } => end.map(PreciseDateTimeResult::TimeZone), - DateTimeRange::AmbiguousStart { - ambiguous_start: _, - end, - } => Some(PreciseDateTimeResult::TimeZone(*end)), - DateTimeRange::AmbiguousEnd { .. } => None, } } @@ -863,14 +822,6 @@ impl DateTimeRange { }, } } - /// Returns true, if one value in the range is ambiguous. - /// - /// For details see [DateTimeRange::AmbiguousEnd] or [DateTimeRange::AmbiguousStart] - pub fn is_ambiguous(&self) -> bool { - matches! {self, - &DateTimeRange::AmbiguousEnd { .. } | &DateTimeRange::AmbiguousStart { .. } - } - } } /** @@ -943,6 +894,187 @@ pub fn parse_time_range(buf: &[u8]) -> Result { } } +/// The Dicom standard allows for parsing a date-time range in which one DT value provides time-zone +/// information but the other does not. +/// +/// Example '19750101-19800101+0200'. +/// +/// In such case, the missing time-zone can be interpreted as the local time-zone or the time-zone +/// provided with the upper bound (or something else altogether). +/// This trait is implemented by parsers handling the afformentioned situation. +pub trait AmbiguousDtRangeParser { + /// Retrieve a [DateTimeRange] if the lower range bound is missing a time-zone + fn parse_with_ambiguous_start( + ambiguous_start: NaiveDateTime, + end: DateTime, + ) -> Result; + /// Retrieve a [DateTimeRange] if the upper range bound is missing a time-zone + fn parse_with_ambiguous_end( + start: DateTime, + ambiguous_end: NaiveDateTime, + ) -> Result; +} + +/// Use time-zone information from the time-zone aware value. +/// Retrieves a [DateTimeRange::TimeZone] (Default). +#[derive(Debug)] +pub struct ToKnownTimeZone; + +/// Use time-zone information of the local system clock. +/// Retrieves a [DateTimeRange::TimeZone]. +#[derive(Debug)] +pub struct ToLocalTimeZone; + +/// Discard known, parsed time-zone information. +/// Retrieves a [DateTimeRange::Naive]. +#[derive(Debug)] +pub struct IgnoreTimeZone; + +impl AmbiguousDtRangeParser for ToKnownTimeZone { + fn parse_with_ambiguous_start( + ambiguous_start: NaiveDateTime, + end: DateTime, + ) -> Result { + 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, + ambiguous_end: NaiveDateTime, + ) -> Result { + 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 ToLocalTimeZone { + fn parse_with_ambiguous_start( + ambiguous_start: NaiveDateTime, + end: DateTime, + ) -> Result { + 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, + ambiguous_end: NaiveDateTime, + ) -> Result { + 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, + ) -> Result { + 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, + ambiguous_end: NaiveDateTime, + ) -> Result { + 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), + }) + } + } +} + /// Looks for a range separator '-'. /// Returns a `DateTimeRange`. /// If the parser encounters two date-time values, where one is time-zone aware and the other is not, @@ -955,6 +1087,16 @@ pub fn parse_time_range(buf: &[u8]) -> Result { /// In such cases, two '-' characters are present and the parser will favor the first one as a range separator, /// if it produces a valid `DateTimeRange`. Otherwise, it tries the second one. pub fn parse_datetime_range(buf: &[u8]) -> Result { + parse_datetime_range_impl::(buf) +} + +/// Same as [parse_datetime_range()] but allows for custom handling of ambiguous Date-time ranges. +/// See [AmbiguousDtRangeParser]. +pub fn parse_datetime_range_custom(buf: &[u8]) -> Result { + parse_datetime_range_impl::(buf) +} + +pub fn parse_datetime_range_impl(buf: &[u8]) -> Result { // minimum length of one valid DicomDateTime (YYYY) and one '-' separator if buf.len() < 5 { return UnexpectedEndOfElementSnafu.fail(); @@ -1018,18 +1160,12 @@ pub fn parse_datetime_range(buf: &[u8]) -> Result { // lower bound time-zone was missing PreciseDateTimeResult::Naive(start), PreciseDateTimeResult::TimeZone(end), - ) => Ok(DateTimeRange::AmbiguousStart { - ambiguous_start: start, - end, - }), + ) => T::parse_with_ambiguous_start(start, end), ( PreciseDateTimeResult::TimeZone(start), // upper bound time-zone was missing PreciseDateTimeResult::Naive(end), - ) => Ok(DateTimeRange::AmbiguousEnd { - start, - ambiguous_end: end, - }), + ) => T::parse_with_ambiguous_end(start, end), }; match dtr { Ok(val) => return Ok(val), @@ -1060,17 +1196,11 @@ pub fn parse_datetime_range(buf: &[u8]) -> Result { } // lower bound time-zone was missing (PreciseDateTimeResult::Naive(start), PreciseDateTimeResult::TimeZone(end)) => { - Ok(DateTimeRange::AmbiguousStart { - ambiguous_start: start, - end, - }) + T::parse_with_ambiguous_start(start, end) } // upper bound time-zone was missing (PreciseDateTimeResult::TimeZone(start), PreciseDateTimeResult::Naive(end)) => { - Ok(DateTimeRange::AmbiguousEnd { - start, - ambiguous_end: end, - }) + T::parse_with_ambiguous_end(start, end) } } } @@ -1606,21 +1736,24 @@ mod tests { }) ); // one 'west' Time zone offset gets parsed, offset cannot be mistaken for a date-time - // the missing Time zone offset will result in an ambiguous range variant + // the missing Time zone offset will be replaced with the existing one (default behavior) assert_eq!( parse_datetime_range(b"19900101-1200-1999").unwrap(), - DateTimeRange::AmbiguousEnd { - start: FixedOffset::west_opt(12 * 3600) + 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(), - ambiguous_end: NaiveDateTime::new( + .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() + ) } ); // '0500' can either be a valid west UTC offset on the lower bound, or a valid date-time on the upper bound @@ -1628,18 +1761,22 @@ mod tests { // and will result in an ambiguous range variant. assert_eq!( parse_datetime_range(b"0050-0500-1000").unwrap(), - DateTimeRange::AmbiguousStart { - ambiguous_start: NaiveDateTime::new( - NaiveDate::from_ymd_opt(50, 1, 1).unwrap(), + DateTimeRange::TimeZone { + start: + Some(FixedOffset::west_opt(10 * 3600) + .unwrap() + .from_local_datetime(&NaiveDateTime::new( + NaiveDate::from_ymd_opt(50, 1, 1).unwrap(), NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() - ), - end: FixedOffset::west_opt(10 * 3600) + )) + .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() + .unwrap()) } ); // sequence with more than 3 dashes '-' is refused. From cc30c988d11f52f77266cf9069c370f7dd1286d3 Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Tue, 13 Feb 2024 15:10:23 +0100 Subject: [PATCH 09/23] - add possibility to fail on ambiguous DT range --- core/src/value/primitive.rs | 12 ++++++++++-- core/src/value/range.rs | 31 ++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/core/src/value/primitive.rs b/core/src/value/primitive.rs index 126c10a20..9d7dc41f7 100644 --- a/core/src/value/primitive.rs +++ b/core/src/value/primitive.rs @@ -3065,7 +3065,7 @@ impl PrimitiveValue { /// ``` /// # use dicom_core::value::{C, PrimitiveValue}; /// # use std::error::Error; - /// use dicom_core::value::range::{AmbiguousDtRangeParser, ToLocalTimeZone, IgnoreTimeZone, DateTimeRange}; + /// use dicom_core::value::range::{AmbiguousDtRangeParser, ToLocalTimeZone, IgnoreTimeZone, FailOnAmbiguousRange, DateTimeRange}; /// use chrono::{NaiveDate, NaiveTime, NaiveDateTime}; /// # fn main() -> Result<(), Box> { /// @@ -3086,7 +3086,7 @@ impl PrimitiveValue { /// .offset() /// ); /// - /// // ignores parsed time-zone, retrieve naive range + /// // ignore parsed time-zone, retrieve a time-zone naive range /// let naive_range = PrimitiveValue::from("1992+0599-1993") /// .to_datetime_range_custom::()?; /// @@ -3104,6 +3104,14 @@ impl PrimitiveValue { /// ).unwrap() /// ); /// + /// // fail upon parsing a ambiguous DT range + /// assert!( + /// PrimitiveValue::from("1992+0599-1993") + /// .to_datetime_range_custom::().is_err() + /// ); + /// + /// + /// /// # Ok(()) /// # } /// ``` diff --git a/core/src/value/range.rs b/core/src/value/range.rs index 952631c2c..e42818021 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -2,7 +2,7 @@ //! Parsing into ranges happens via partial precision structures (DicomDate, DicomTime, //! DicomDatime) so ranges can handle null components in date, time, date-time values. use chrono::{ - DateTime, FixedOffset, Local, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, + DateTime, FixedOffset, Local, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone }; use snafu::{Backtrace, OptionExt, ResultExt, Snafu}; @@ -82,6 +82,10 @@ pub enum Error { "Trying to convert a time-zone aware date-time value to a time-zone unaware value" ))] DateTimeTzAware { 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 = std::result::Result; @@ -920,6 +924,10 @@ pub trait AmbiguousDtRangeParser { #[derive(Debug)] pub struct ToKnownTimeZone; +/// Fail if ambiguous date-time range is parsed +#[derive(Debug)] +pub struct FailOnAmbiguousRange; + /// Use time-zone information of the local system clock. /// Retrieves a [DateTimeRange::TimeZone]. #[derive(Debug)] @@ -983,6 +991,27 @@ impl AmbiguousDtRangeParser for ToKnownTimeZone { } } +impl AmbiguousDtRangeParser for FailOnAmbiguousRange{ + fn parse_with_ambiguous_end( + start: DateTime, + end: NaiveDateTime, + ) -> Result { + 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, + ) -> Result { + 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, From 73c35829ccd4d6e76c220644744421da54b86676 Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Tue, 13 Feb 2024 15:26:12 +0100 Subject: [PATCH 10/23] - primitive value of da, tm, dt can be retrieved as it's range --- core/src/value/primitive.rs | 50 ++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/core/src/value/primitive.rs b/core/src/value/primitive.rs index 9d7dc41f7..7ef966ab9 100644 --- a/core/src/value/primitive.rs +++ b/core/src/value/primitive.rs @@ -2,7 +2,7 @@ //! //! See [`PrimitiveValue`](./enum.PrimitiveValue.html). -use super::DicomValueType; +use super::{AsRange, DicomValueType}; use crate::header::{HasLength, Length, Tag}; use crate::value::partial::{DateComponent, DicomDate, DicomDateTime, DicomTime}; use crate::value::person_name::PersonName; @@ -2804,7 +2804,7 @@ impl PrimitiveValue { } /// Retrieve a single `DateRange` from this value. /// - /// If the value is already represented as a `DicomDate`, it is converted into `DateRange` - todo. + /// If the value is already represented as a `DicomDate`, it is converted into `DateRange`. /// If the value is a string or sequence of strings, /// the first string is decoded to obtain a `DateRange`, potentially failing if the /// string does not represent a valid `DateRange`. @@ -2841,6 +2841,14 @@ impl PrimitiveValue { /// ``` pub fn to_date_range(&self) -> Result { match self { + PrimitiveValue::Date(da) if !da.is_empty() => da[0] + .range() + .context(ParseDateRangeSnafu) + .map_err(|err| ConvertValueError { + requested: "DateRange", + original: self.value_type(), + cause: Some(err), + }), PrimitiveValue::Str(s) => super::range::parse_date_range(s.trim_end().as_bytes()) .context(ParseDateRangeSnafu) .map_err(|err| ConvertValueError { @@ -2876,7 +2884,7 @@ impl PrimitiveValue { /// Retrieve a single `TimeRange` from this value. /// - /// If the value is already represented as a `DicomTime`, it is converted into `TimeRange` - todo. + /// If the value is already represented as a `DicomTime`, it is converted into a `TimeRange`. /// If the value is a string or sequence of strings, /// the first string is decoded to obtain a `TimeRange`, potentially failing if the /// string does not represent a valid `DateRange`. @@ -2916,6 +2924,14 @@ impl PrimitiveValue { /// ``` pub fn to_time_range(&self) -> Result { match self { + PrimitiveValue::Time(t) if !t.is_empty() => t[0] + .range() + .context(ParseTimeRangeSnafu) + .map_err(|err| ConvertValueError { + requested: "TimeRange", + original: self.value_type(), + cause: Some(err), + }), PrimitiveValue::Str(s) => super::range::parse_time_range(s.trim_end().as_bytes()) .context(ParseTimeRangeSnafu) .map_err(|err| ConvertValueError { @@ -3022,6 +3038,14 @@ impl PrimitiveValue { /// ``` pub fn to_datetime_range(&self) -> Result { match self { + PrimitiveValue::DateTime(dt) if !dt.is_empty() => dt[0] + .range() + .context(ParseDateTimeRangeSnafu) + .map_err(|err| ConvertValueError { + requested: "DateTimeRange", + original: self.value_type(), + cause: Some(err), + }), PrimitiveValue::Str(s) => super::range::parse_datetime_range(s.trim_end().as_bytes()) .context(ParseDateTimeRangeSnafu) .map_err(|err| ConvertValueError { @@ -3059,7 +3083,7 @@ impl PrimitiveValue { /// /// Use a custom ambiguous date-time range parser. /// - /// See [PrimitiveValue::to_datetime_range] and [AmbiguousDtRangeParser] + /// For full description see [PrimitiveValue::to_datetime_range] and [AmbiguousDtRangeParser]. /// # Example /// /// ``` @@ -3085,7 +3109,7 @@ impl PrimitiveValue { /// .as_datetime_with_time_zone().unwrap() /// .offset() /// ); - /// + /// /// // ignore parsed time-zone, retrieve a time-zone naive range /// let naive_range = PrimitiveValue::from("1992+0599-1993") /// .to_datetime_range_custom::()?; @@ -3103,15 +3127,15 @@ impl PrimitiveValue { /// ) /// ).unwrap() /// ); - /// + /// /// // fail upon parsing a ambiguous DT range /// assert!( /// PrimitiveValue::from("1992+0599-1993") /// .to_datetime_range_custom::().is_err() /// ); - /// - /// - /// + /// + /// + /// /// # Ok(()) /// # } /// ``` @@ -3119,6 +3143,14 @@ impl PrimitiveValue { &self, ) -> Result { match self { + PrimitiveValue::DateTime(dt) if !dt.is_empty() => dt[0] + .range() + .context(ParseDateTimeRangeSnafu) + .map_err(|err| ConvertValueError { + requested: "DateTimeRange", + original: self.value_type(), + cause: Some(err), + }), PrimitiveValue::Str(s) => { super::range::parse_datetime_range_custom::(s.trim_end().as_bytes()) .context(ParseDateTimeRangeSnafu) From 219814daba515f628e310285abecf1ebe7b7594b Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Thu, 15 Feb 2024 14:17:51 +0100 Subject: [PATCH 11/23] - as default, ambiguous DT range uses local clock time-zone - doc changes --- core/src/value/partial.rs | 12 ++- core/src/value/primitive.rs | 19 ++-- core/src/value/range.rs | 179 ++++++++++++++++++++---------------- 3 files changed, 118 insertions(+), 92 deletions(-) diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 57d1e906d..00efbd3dc 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -75,11 +75,13 @@ pub enum DateComponent { Millisecond, // microsecond (full second fraction) Fraction, + // West UTC time-zone offset UtcWest, + // East UTC time-zone offset UtcEast, } -/// Represents a Dicom Date value with a partial precision, +/// Represents a Dicom date (DA) value with a partial precision, /// where some date components may be missing. /// /// Unlike [chrono::NaiveDate], it does not allow for negative years. @@ -113,7 +115,7 @@ pub enum DateComponent { #[derive(Clone, Copy, PartialEq)] pub struct DicomDate(DicomDateImpl); -/// Represents a Dicom Time value with a partial precision, +/// Represents a Dicom time (TM) value with a partial precision, /// where some time components may be missing. /// /// Unlike [chrono::NaiveTime], this implemenation has only 6 digit precision @@ -181,14 +183,14 @@ enum DicomTimeImpl { Fraction(u8, u8, u8, u32, u8), } -/// Represents a Dicom DateTime value with a partial precision, +/// Represents a Dicom date-time (DT) value with a partial precision, /// where some date or time components may be missing. /// /// `DicomDateTime` is always internally represented by a [DicomDate]. /// The [DicomTime] and a timezone [FixedOffset] values are optional. /// -/// It implements [AsRange] trait, which serves to access usable precise values from `DicomDateTime` values -/// with missing components in the form of [PreciseDateTimeResult]. +/// It implements [AsRange] trait, which serves to retrieve a [PreciseDateTimeResult] from values +/// with missing components. /// # Example /// ``` /// # use std::error::Error; diff --git a/core/src/value/primitive.rs b/core/src/value/primitive.rs index 7ef966ab9..94cfda106 100644 --- a/core/src/value/primitive.rs +++ b/core/src/value/primitive.rs @@ -2978,7 +2978,7 @@ impl PrimitiveValue { /// /// ``` /// # use dicom_core::value::{C, PrimitiveValue}; - /// use chrono::{DateTime, NaiveDate, NaiveTime, NaiveDateTime, FixedOffset, TimeZone}; + /// use chrono::{DateTime, NaiveDate, NaiveTime, NaiveDateTime, FixedOffset, TimeZone, Local}; /// # use std::error::Error; /// use dicom_core::value::{DateTimeRange, PreciseDateTimeResult}; /// @@ -3011,14 +3011,15 @@ impl PrimitiveValue { /// /// let lower = PrimitiveValue::from("2012-").to_datetime_range()?; /// + /// // range has no upper bound /// assert!(lower.end().is_none()); /// /// // The DICOM protocol allows for parsing text representations of date-time ranges, /// // where one bound has a time-zone but the other has not. /// let dt_range = PrimitiveValue::from("1992+0500-1993").to_datetime_range()?; /// - /// // the default behavior in this case is to use the known time-zone to construct - /// // two time-zone aware DT bounds. + /// // the default behavior in this case is to use the local clock time-zone offset + /// // in place of the missing time-zone. This can be customized with [to_datetime_range_custom()] /// assert_eq!( /// dt_range, /// DateTimeRange::TimeZone{ @@ -3026,7 +3027,7 @@ impl PrimitiveValue { /// .ymd_opt(1992, 1, 1).unwrap() /// .and_hms_micro_opt(0, 0, 0, 0).unwrap() /// ), - /// end: Some(FixedOffset::east_opt(5*3600).unwrap() + /// end: Some(Local::now().offset() /// .ymd_opt(1993, 12, 31).unwrap() /// .and_hms_micro_opt(23, 59, 59, 999_999).unwrap() /// ) @@ -3128,7 +3129,7 @@ impl PrimitiveValue { /// ).unwrap() /// ); /// - /// // fail upon parsing a ambiguous DT range + /// // always fail upon parsing an ambiguous DT range /// assert!( /// PrimitiveValue::from("1992+0599-1993") /// .to_datetime_range_custom::().is_err() @@ -4311,7 +4312,7 @@ mod tests { use crate::value::partial::{DicomDate, DicomDateTime, DicomTime}; use crate::value::range::{DateRange, DateTimeRange, TimeRange}; use crate::value::{PrimitiveValue, ValueType}; - use chrono::{FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone}; + use chrono::{FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone}; use smallvec::smallvec; #[test] @@ -4926,15 +4927,15 @@ mod tests { .unwrap() ); // East UTC offset gets parsed and the missing lower bound time-zone - // will be the same as the parsed offset value + // will be the local clock time-zone offset assert_eq!( PrimitiveValue::from(&b"2020-2030+0800"[..]) .to_datetime_range() .unwrap(), DateTimeRange::TimeZone { start: Some( - FixedOffset::east_opt(8 * 3600) - .unwrap() + Local::now() + .offset() .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(), NaiveTime::from_hms_opt(0, 0, 0).unwrap() diff --git a/core/src/value/range.rs b/core/src/value/range.rs index e42818021..c2bd54a61 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -2,7 +2,7 @@ //! Parsing into ranges happens via partial precision structures (DicomDate, DicomTime, //! DicomDatime) so ranges can handle null components in date, time, date-time values. use chrono::{ - DateTime, FixedOffset, Local, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone + DateTime, FixedOffset, Local, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, }; use snafu::{Backtrace, OptionExt, ResultExt, Snafu}; @@ -67,31 +67,29 @@ pub enum Error { "Date-time does not contain time-zone, cannot convert to time-zone aware value" ))] NoTimeZone { backtrace: Backtrace }, - #[snafu(display( - "Failed to convert to a time-zone aware date-time value with ambiguous results: {t1}, {t2} " - ))] - DateTimeAmbiguous { - t1: DateTime, - t2: DateTime, - }, #[snafu(display( "Failed to convert to a time-zone aware date-time value: Given local time representation is invalid" ))] - DateTimeInvalid { backtrace: Backtrace }, + ToPreciseDateTime { backtrace: Backtrace }, #[snafu(display( - "Trying to convert a time-zone aware date-time value to a time-zone unaware value" + "Use 'to_datetime_with_time_zone' or 'to_naive_date_time' to retrieve a precise value from a date-time" ))] DateTimeTzAware { 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 }, + AmbiguousDtRange { + start: NaiveDateTime, + end: NaiveDateTime, + time_zone: FixedOffset, + backtrace: Backtrace, + }, } type Result = std::result::Result; -/// The DICOM protocol accepts date (DA) / time(TM) / date-time(DT) values with null components. +/// The DICOM protocol accepts date (DA) / time (TM) / date-time (DT) values with null components. /// -/// Imprecise values are to be handled as date / time ranges. +/// Imprecise values are to be handled as ranges. /// /// This trait is implemented by date / time structures with partial precision. /// @@ -134,36 +132,35 @@ type Result = std::result::Result; /// /// let primitive = PrimitiveValue::from("199402"); /// -/// // This is the fastest way to get to a useful value, but it fails not only for invalid +/// // This is the fastest way to get to a useful date value, but it fails not only for invalid /// // dates but for imprecise ones as well. /// assert!(primitive.to_naive_date().is_err()); /// -/// // We should take indermediary steps ... +/// // Take intermediate steps: /// +/// // Retrieve a DicomDate. /// // The parser now checks for basic year and month value ranges here. /// // But, it would not detect invalid dates like 30th of february etc. /// let dicom_date : DicomDate = primitive.to_date()?; /// -/// // now we have a valid DicomDate value, let's check if it's precise. +/// // as we have a valid DicomDate value, let's check if it's precise. /// if dicom_date.is_precise(){ /// // no components are missing, we can proceed by calling .exact() /// // which calls the `chrono` library /// let precise_date: NaiveDate = dicom_date.exact()?; /// } /// else{ -/// // day and / or month are missing, no need to call expensive .exact() method -/// // - it will fail -/// // try to retrieve the date range instead +/// // day / month are missing, no need to call the expensive .exact() method - it will fail +/// // retrieve the earliest possible value directly from DicomDate +/// let earliest: NaiveDate = dicom_date.earliest()?; +/// +/// // or convert the date to a date range instead /// let date_range: DateRange = dicom_date.range()?; /// -/// // the real conversion to a `chrono` value only happens at this stage /// if let Some(start) = date_range.start(){ /// // the range has a given lower date bound /// } /// -/// // or try to retrieve the earliest possible value directly from DicomDate -/// let earliest: NaiveDate = dicom_date.earliest()?; -/// /// } /// /// # Ok(()) @@ -471,7 +468,7 @@ impl DicomDateTime { note = "Use `to_datetime_with_time_zone` or `to_naive_date_time`" )] pub fn to_chrono_datetime(self) -> Result> { - DateTimeInvalidSnafu.fail() + ToPreciseDateTimeSnafu.fail() } } @@ -513,8 +510,6 @@ pub struct TimeRange { /// two [`Option>`] values. /// [None] means no upper or no lower bound for range is present. /// -/// In borderline cases the parser produces [DateTimeRange::AmbiguousStart] and [DateTimeRange::AmbiguousEnd]. -/// /// # Example /// ``` /// # use std::error::Error; @@ -551,7 +546,7 @@ pub enum DateTimeRange { TimeZone { start: Option>, end: Option>, - } + }, } /// A precise date-time value, that can either be time-zone aware or time-zone naive. @@ -919,8 +914,18 @@ pub trait AmbiguousDtRangeParser { ) -> Result; } +/// For the missing time-zone use time-zone information of the local system clock. +/// Retrieves a [DateTimeRange::TimeZone]. +/// +/// Because "A Date Time value without the optional suffix is interpreted to be in the local time zone +/// of the application creating the Data Element, unless explicitly specified by the Timezone Offset From UTC (0008,0201).", +/// this is the default behavior of the parser. +/// https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html +#[derive(Debug)] +pub struct ToLocalTimeZone; + /// Use time-zone information from the time-zone aware value. -/// Retrieves a [DateTimeRange::TimeZone] (Default). +/// Retrieves a [DateTimeRange::TimeZone]. #[derive(Debug)] pub struct ToKnownTimeZone; @@ -928,12 +933,7 @@ pub struct ToKnownTimeZone; #[derive(Debug)] pub struct FailOnAmbiguousRange; -/// Use time-zone information of the local system clock. -/// Retrieves a [DateTimeRange::TimeZone]. -#[derive(Debug)] -pub struct ToLocalTimeZone; - -/// Discard known, parsed time-zone information. +/// Discard known (parsed) time-zone information. /// Retrieves a [DateTimeRange::Naive]. #[derive(Debug)] pub struct IgnoreTimeZone; @@ -991,24 +991,32 @@ impl AmbiguousDtRangeParser for ToKnownTimeZone { } } -impl AmbiguousDtRangeParser for FailOnAmbiguousRange{ +impl AmbiguousDtRangeParser for FailOnAmbiguousRange { fn parse_with_ambiguous_end( - start: DateTime, - end: NaiveDateTime, - ) -> Result { + start: DateTime, + end: NaiveDateTime, + ) -> Result { let time_zone = *start.offset(); let start = start.naive_local(); - AmbiguousDtRangeSnafu{ start, end, time_zone}.fail() - + AmbiguousDtRangeSnafu { + start, + end, + time_zone, + } + .fail() } fn parse_with_ambiguous_start( - start: NaiveDateTime, - end: DateTime, - ) -> Result { + start: NaiveDateTime, + end: DateTime, + ) -> Result { let time_zone = *end.offset(); let end = end.naive_local(); - AmbiguousDtRangeSnafu{ start, end, time_zone}.fail() - + AmbiguousDtRangeSnafu { + start, + end, + time_zone, + } + .fail() } } @@ -1106,8 +1114,17 @@ impl AmbiguousDtRangeParser for IgnoreTimeZone { /// Looks for a range separator '-'. /// Returns a `DateTimeRange`. +/// /// If the parser encounters two date-time values, where one is time-zone aware and the other is not, -/// it will produce an ambiguous DateTimeRange variant. +/// it will use the local time-zone offset and use it instead of the missing time-zone. +/// +/// Because "A Date Time value without the optional suffix is interpreted to be in the local time zone +/// of the application creating the Data Element, unless explicitly specified by the Timezone Offset From UTC (0008,0201).", +/// this is the default behavior of the parser. +/// https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html +/// +/// To customize this behavior, please use [parse_datetime_range_custom()]. +/// /// Users are advised, that for very specific inputs, inconsistent behavior can occur. /// This behavior can only be produced when all of the following is true: /// - two very short date-times in the form of YYYY are presented (YYYY-YYYY) @@ -1116,7 +1133,7 @@ impl AmbiguousDtRangeParser for IgnoreTimeZone { /// In such cases, two '-' characters are present and the parser will favor the first one as a range separator, /// if it produces a valid `DateTimeRange`. Otherwise, it tries the second one. pub fn parse_datetime_range(buf: &[u8]) -> Result { - parse_datetime_range_impl::(buf) + parse_datetime_range_impl::(buf) } /// Same as [parse_datetime_range()] but allows for custom handling of ambiguous Date-time ranges. @@ -1765,47 +1782,53 @@ mod tests { }) ); // one 'west' Time zone offset gets parsed, offset cannot be mistaken for a date-time - // the missing Time zone offset will be replaced with the existing one (default behavior) + // the missing Time zone offset will be replaced with local clock time-zone offset (default behavior) 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(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() - ) + 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() + ) } ); // '0500' can either be a valid west UTC offset on the lower bound, or a valid date-time on the upper bound // Now, the first dash is considered to be a range separator, so the lower bound time-zone offset is missing - // and will result in an ambiguous range variant. + // and will be considered to be the local clock time-zone offset. assert_eq!( parse_datetime_range(b"0050-0500-1000").unwrap(), DateTimeRange::TimeZone { - start: - Some(FixedOffset::west_opt(10 * 3600) - .unwrap() - .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()) + 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() + ) } ); // sequence with more than 3 dashes '-' is refused. From e784fdabf2f0a621f8ab6600135c082168979434 Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Wed, 21 Feb 2024 19:22:02 +0100 Subject: [PATCH 12/23] - fix deprecation info - fix doctest --- core/src/value/deserialize.rs | 2 +- core/src/value/partial.rs | 8 ++++---- core/src/value/primitive.rs | 28 +++++++++++++--------------- core/src/value/range.rs | 2 +- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/core/src/value/deserialize.rs b/core/src/value/deserialize.rs index 2f504a88e..4d258b1e3 100644 --- a/core/src/value/deserialize.rs +++ b/core/src/value/deserialize.rs @@ -337,7 +337,7 @@ where * For DateTime with missing components, or if exact second fraction accuracy needs to be preserved, use `parse_datetime_partial`. */ -#[deprecated(since = "0.6.4", note = "Only use parse_datetime_partial()")] +#[deprecated(since = "0.7.0", note = "Only use parse_datetime_partial()")] pub fn parse_datetime(buf: &[u8], dt_utc_offset: FixedOffset) -> Result> { let date = parse_date(buf)?; let buf = &buf[8..]; diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 00efbd3dc..0326cef38 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -331,7 +331,7 @@ impl DicomDate { } } - /** Rertrieves the last fully precise `DateComponent` of the value */ + /** Retrieves the last fully precise `DateComponent` of the value */ pub(crate) fn precision(&self) -> DateComponent { match self { DicomDate(DicomDateImpl::Year(..)) => DateComponent::Year, @@ -527,7 +527,7 @@ impl DicomTime { ))) } - /** Rertrieves the last fully precise `DateComponent` of the value */ + /** Retrieves the last fully precise `DateComponent` of the value */ pub(crate) fn precision(&self) -> DateComponent { match self { DicomTime(DicomTimeImpl::Hour(..)) => DateComponent::Hour, @@ -691,8 +691,8 @@ impl DicomDateTime { self.time_zone.is_some() } - /** Retrieves a refrence to the internal offset value */ - #[deprecated(since = "0.6.4", note = "Use `time_zone` instead")] + /** Retrieves a reference to the internal offset value */ + #[deprecated(since = "0.7.0", note = "Use `time_zone` instead")] pub fn offset(&self) {} } diff --git a/core/src/value/primitive.rs b/core/src/value/primitive.rs index 94cfda106..8cc317fa9 100644 --- a/core/src/value/primitive.rs +++ b/core/src/value/primitive.rs @@ -2640,9 +2640,9 @@ impl PrimitiveValue { } } - #[deprecated(since = "0.6.4", note = "Use `to_datetime` instead")] + #[deprecated(since = "0.7.0", note = "Use `to_datetime` instead")] pub fn to_chrono_datetime(&self) {} - #[deprecated(since = "0.6.4", note = "Use `to_multi_datetime` instead")] + #[deprecated(since = "0.7.0", note = "Use `to_multi_datetime` instead")] pub fn to_multi_chrono_datetime(&self) {} /// Retrieve a single `DicomDateTime` from this value. @@ -3014,12 +3014,11 @@ impl PrimitiveValue { /// // range has no upper bound /// assert!(lower.end().is_none()); /// - /// // The DICOM protocol allows for parsing text representations of date-time ranges, - /// // where one bound has a time-zone but the other has not. + /// // One time-zone in a range is missing /// let dt_range = PrimitiveValue::from("1992+0500-1993").to_datetime_range()?; /// - /// // the default behavior in this case is to use the local clock time-zone offset - /// // in place of the missing time-zone. This can be customized with [to_datetime_range_custom()] + /// // It will be replaced with the local clock time-zone offset + /// // This can be customized with [to_datetime_range_custom()] /// assert_eq!( /// dt_range, /// DateTimeRange::TimeZone{ @@ -3090,19 +3089,18 @@ impl PrimitiveValue { /// ``` /// # use dicom_core::value::{C, PrimitiveValue}; /// # use std::error::Error; - /// use dicom_core::value::range::{AmbiguousDtRangeParser, ToLocalTimeZone, IgnoreTimeZone, FailOnAmbiguousRange, DateTimeRange}; + /// use dicom_core::value::range::{AmbiguousDtRangeParser, ToKnownTimeZone, IgnoreTimeZone, FailOnAmbiguousRange, DateTimeRange}; /// use chrono::{NaiveDate, NaiveTime, NaiveDateTime}; /// # fn main() -> Result<(), Box> { /// - /// // The DICOM protocol allows for parsing text representations of date-time ranges, - /// // where one bound has a time-zone but the other has not. - /// // the default behavior in this case is to use the known time-zone to construct - /// // two time-zone aware DT bounds. But we want to use the local clock time-zone instead - /// let dt_range = PrimitiveValue::from("1992+0599-1993") - /// .to_datetime_range_custom::()?; + /// // The upper bound time-zone is missing + /// // the default behavior in this case is to use the local clock time-zone. + /// // But we want to use the known (parsed) time-zone from the lower bound instead. + /// let dt_range = PrimitiveValue::from("1992+0500-1993") + /// .to_datetime_range_custom::()?; /// - /// // local clock time-zone in the upper bound should be different from 0599 in the lower bound. - /// assert_ne!( + /// // values are in the same time-zone + /// assert_eq!( /// dt_range.start().unwrap() /// .as_datetime_with_time_zone().unwrap() /// .offset(), diff --git a/core/src/value/range.rs b/core/src/value/range.rs index c2bd54a61..71e54486f 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -464,7 +464,7 @@ impl DicomDateTime { } #[deprecated( - since = "0.6.4", + since = "0.7.0", note = "Use `to_datetime_with_time_zone` or `to_naive_date_time`" )] pub fn to_chrono_datetime(self) -> Result> { From 1a8621bc04fee7d2c211dc0aa706b359635a43f7 Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Wed, 21 Feb 2024 20:12:58 +0100 Subject: [PATCH 13/23] - simplify DicomDateTime api --- core/src/value/range.rs | 101 ++++++++++++---------------------------- 1 file changed, 29 insertions(+), 72 deletions(-) diff --git a/core/src/value/range.rs b/core/src/value/range.rs index 71e54486f..e497a9348 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -2,7 +2,7 @@ //! Parsing into ranges happens via partial precision structures (DicomDate, DicomTime, //! DicomDatime) so ranges can handle null components in date, time, date-time values. use chrono::{ - DateTime, FixedOffset, Local, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, + DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, }; use snafu::{Backtrace, OptionExt, ResultExt, Snafu}; @@ -63,18 +63,8 @@ pub enum Error { f: u32, backtrace: Backtrace, }, - #[snafu(display( - "Date-time does not contain time-zone, cannot convert to time-zone aware value" - ))] - NoTimeZone { backtrace: Backtrace }, - #[snafu(display( - "Failed to convert to a time-zone aware date-time value: Given local time representation is invalid" - ))] + #[snafu(display("Use 'to_precise_datetime' to retrieve a precise value from a date-time"))] ToPreciseDateTime { backtrace: Backtrace }, - #[snafu(display( - "Use 'to_datetime_with_time_zone' or 'to_naive_date_time' to retrieve a precise value from a date-time" - ))] - DateTimeTzAware { 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.'" ))] @@ -173,7 +163,7 @@ pub trait AsRange { /// returns true if value has all possible date / time components fn is_precise(&self) -> bool; - /// Returns a corresponding `chrono` value, if the partial precision structure has full accuracy. + /// Returns a corresponding precise value, if the partial precision structure has full accuracy. fn exact(&self) -> Result { if self.is_precise() { Ok(self.earliest()?) @@ -182,12 +172,12 @@ pub trait AsRange { } } - /// Returns the earliest possible valid `chrono` value from a partial precision structure. + /// Returns the earliest possible value from a partial precision structure. /// Missing components default to 1 (days, months) or 0 (hours, minutes, ...) /// If structure contains invalid combination of `DateComponent`s, it fails. fn earliest(&self) -> Result; - /// Returns the latest possible valid `chrono` value from a partial precision structure. + /// Returns the latest possible value from a partial precision structure. /// If structure contains invalid combination of `DateComponent`s, it fails. fn latest(&self) -> Result; @@ -389,20 +379,15 @@ impl AsRange for DicomDateTime { let start = self.earliest()?; let end = self.latest()?; - if start.has_time_zone() { - let s = start.into_datetime_with_time_zone()?; - let e = end.into_datetime_with_time_zone()?; - Ok(DateTimeRange::TimeZone { - start: Some(s), - end: Some(e), - }) - } else { - let s = start.into_datetime()?; - let e = end.into_datetime()?; - Ok(DateTimeRange::Naive { - start: Some(s), - end: Some(e), - }) + match (start, end) { + (PreciseDateTimeResult::Naive(start), PreciseDateTimeResult::Naive(end)) => { + DateTimeRange::from_start_to_end(start, end) + } + (PreciseDateTimeResult::TimeZone(start), PreciseDateTimeResult::TimeZone(end)) => { + DateTimeRange::from_start_to_end_with_time_zone(start, end) + } + + _ => unreachable!(), } } } @@ -430,43 +415,14 @@ impl DicomTime { } impl DicomDateTime { - /// Retrieves a `chrono::DateTime`. - /// It the value does not store a time-zone or the date-time value is not precise or the conversion leads to ambiguous results, + /// Retrieves a [PreciseDateTimeResult] from a date-time value. + /// If the date-time value is not precise or the conversion leads to ambiguous results, /// it fails. - /// To inspect the possibly ambiguous results of this conversion, see `to_chrono_local_result` - pub fn to_datetime_with_time_zone(self) -> Result> { - self.exact()?.into_datetime_with_time_zone() - } - - /// Retrieves a `chrono::NaiveDateTime`. If the internal date-time value is not precise or - /// it is time-zone aware, this method will fail. - pub fn to_naive_datetime(&self) -> Result { - self.exact()?.into_datetime() - } - - /// Retrieves a `chrono::LocalResult` by converting the internal time-zone naive date-time representation - /// to a time-zone aware representation. - /// It the value does not store a time-zone or the date-time value is not precise, it fails. - pub fn to_chrono_local_result(self) -> Result>> { - if !self.is_precise() { - return ImpreciseValueSnafu.fail(); - } - - if let Some(offset) = self.time_zone() { - let date = self.date().earliest()?; - let time = self.time().context(ImpreciseValueSnafu)?.earliest()?; - - let naive_date_time = NaiveDateTime::new(date, time); - Ok(offset.from_local_datetime(&naive_date_time)) - } else { - NoTimeZoneSnafu.fail() - } + pub fn to_precise_datetime(&self) -> Result { + self.exact() } - #[deprecated( - since = "0.7.0", - note = "Use `to_datetime_with_time_zone` or `to_naive_date_time`" - )] + #[deprecated(since = "0.7.0", note = "Use `to_precise_date_time()`")] pub fn to_chrono_datetime(self) -> Result> { ToPreciseDateTimeSnafu.fail() } @@ -574,19 +530,19 @@ impl PreciseDateTimeResult { } } - /// Moves out a `chrono::DateTime` if the result is time-zone aware, otherwise it fails. - pub fn into_datetime_with_time_zone(self) -> Result> { + /// Moves out a `chrono::DateTime` if the result is time-zone aware. + pub fn into_datetime_with_time_zone(self) -> Option> { match self { - PreciseDateTimeResult::Naive(..) => NoTimeZoneSnafu.fail(), - PreciseDateTimeResult::TimeZone(value) => Ok(value), + PreciseDateTimeResult::Naive(..) => None, + PreciseDateTimeResult::TimeZone(value) => Some(value), } } - /// Moves out a `chrono::NaiveDateTime` if the result is time-zone naive, otherwise it fails. - pub fn into_datetime(self) -> Result { + /// Moves out a `chrono::NaiveDateTime` if the result is time-zone naive. + pub fn into_datetime(self) -> Option { match self { - PreciseDateTimeResult::Naive(value) => Ok(value), - PreciseDateTimeResult::TimeZone(..) => DateTimeTzAwareSnafu.fail(), + PreciseDateTimeResult::Naive(value) => Some(value), + PreciseDateTimeResult::TimeZone(..) => None, } } @@ -1796,7 +1752,8 @@ mod tests { .unwrap() ), end: Some( - Local::now().offset() + 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() From 5e74ca92708acf941cfceaeeafba9f3faf18d721 Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Fri, 23 Feb 2024 20:35:31 +0100 Subject: [PATCH 14/23] some typos --- core/src/value/partial.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 0326cef38..2db7e7eda 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -671,17 +671,17 @@ impl DicomDateTime { } } - /** Retrieves a refrence to the internal date value */ + /** Retrieves a reference to the internal date value */ pub fn date(&self) -> &DicomDate { &self.date } - /** Retrieves a refrence to the internal time value, if present */ + /** Retrieves a reference to the internal time value, if present */ pub fn time(&self) -> Option<&DicomTime> { self.time.as_ref() } - /** Retrieves a refrence to the internal time-zone value, if present */ + /** Retrieves a reference to the internal time-zone value, if present */ pub fn time_zone(&self) -> Option<&FixedOffset> { self.time_zone.as_ref() } From 7487735966113dd9ce1df862477b5573d24d61de Mon Sep 17 00:00:00 2001 From: Juraj Mlaka Date: Fri, 1 Mar 2024 14:31:07 +0100 Subject: [PATCH 15/23] - rename PreciseDateTimeResult - fix doc links --- core/src/value/mod.rs | 4 +- core/src/value/partial.rs | 20 +++---- core/src/value/primitive.rs | 14 ++--- core/src/value/range.rs | 108 ++++++++++++++++++------------------ object/src/mem.rs | 2 +- 5 files changed, 74 insertions(+), 74 deletions(-) diff --git a/core/src/value/mod.rs b/core/src/value/mod.rs index 0d4e54bcf..a7f849b3a 100644 --- a/core/src/value/mod.rs +++ b/core/src/value/mod.rs @@ -16,7 +16,7 @@ pub mod serialize; pub use self::deserialize::Error as DeserializeError; pub use self::partial::{DicomDate, DicomDateTime, DicomTime}; pub use self::person_name::PersonName; -pub use self::range::{AsRange, DateRange, DateTimeRange, TimeRange, PreciseDateTimeResult}; +pub use self::range::{AsRange, DateRange, DateTimeRange, TimeRange, PreciseDateTime}; pub use self::primitive::{ CastValueError, ConvertValueError, InvalidValueReadError, ModifyValueError, PrimitiveValue, @@ -661,7 +661,7 @@ where } } - /// Retrieves the primitive value as a [`PersonName`][1]. + /// Retrieves the primitive value as a [`PersonName`]. /// /// [1]: super::value::person_name::PersonName pub fn to_person_name(&self) -> Result, ConvertValueError> { diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index 2db7e7eda..bb12f01a7 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -189,14 +189,14 @@ enum DicomTimeImpl { /// `DicomDateTime` is always internally represented by a [DicomDate]. /// The [DicomTime] and a timezone [FixedOffset] values are optional. /// -/// It implements [AsRange] trait, which serves to retrieve a [PreciseDateTimeResult] from values +/// It implements [AsRange] trait, which serves to retrieve a [PreciseDateTime](crate::value::range::PreciseDateTime) from values /// with missing components. /// # Example /// ``` /// # use std::error::Error; /// # use std::convert::TryFrom; /// use chrono::{DateTime, FixedOffset, TimeZone, NaiveDateTime, NaiveDate, NaiveTime}; -/// use dicom_core::value::{DicomDate, DicomTime, DicomDateTime, AsRange, PreciseDateTimeResult}; +/// use dicom_core::value::{DicomDate, DicomTime, DicomDateTime, AsRange, PreciseDateTime}; /// # fn main() -> Result<(), Box> { /// /// let offset = FixedOffset::east_opt(3600).unwrap(); @@ -206,10 +206,10 @@ enum DicomTimeImpl { /// DicomDate::from_y(2020)?, /// offset /// ); -/// // the earliest possible value is output as a [PreciseDateTimeResult] +/// // the earliest possible value is output as a [PreciseDateTime] /// assert_eq!( /// dt.earliest()?, -/// PreciseDateTimeResult::TimeZone( +/// PreciseDateTime::TimeZone( /// offset.from_local_datetime(&NaiveDateTime::new( /// NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(), /// NaiveTime::from_hms_opt(0, 0, 0).unwrap() @@ -217,7 +217,7 @@ enum DicomTimeImpl { /// ); /// assert_eq!( /// dt.latest()?, -/// PreciseDateTimeResult::TimeZone( +/// PreciseDateTime::TimeZone( /// offset.from_local_datetime(&NaiveDateTime::new( /// NaiveDate::from_ymd_opt(2020, 12, 31).unwrap(), /// NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap() @@ -879,7 +879,7 @@ impl DicomDateTime { #[cfg(test)] mod tests { - use crate::value::range::{AsRange, PreciseDateTimeResult}; + use crate::value::range::{AsRange, PreciseDateTime}; use super::*; use chrono::{NaiveDateTime, TimeZone}; @@ -1140,7 +1140,7 @@ mod tests { DicomDateTime::from_date(DicomDate::from_ym(2020, 2).unwrap()) .earliest() .unwrap(), - PreciseDateTimeResult::Naive(NaiveDateTime::new( + PreciseDateTime::Naive(NaiveDateTime::new( NaiveDate::from_ymd_opt(2020, 2, 1).unwrap(), NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap() )) @@ -1153,7 +1153,7 @@ mod tests { ) .latest() .unwrap(), - PreciseDateTimeResult::TimeZone( + PreciseDateTime::TimeZone( FixedOffset::east_opt(0) .unwrap() .from_local_datetime(&NaiveDateTime::new( @@ -1173,7 +1173,7 @@ mod tests { .unwrap() .earliest() .unwrap(), - PreciseDateTimeResult::TimeZone( + PreciseDateTime::TimeZone( FixedOffset::east_opt(0) .unwrap() .from_local_datetime(&NaiveDateTime::new( @@ -1192,7 +1192,7 @@ mod tests { .unwrap() .latest() .unwrap(), - PreciseDateTimeResult::TimeZone( + PreciseDateTime::TimeZone( FixedOffset::east_opt(0) .unwrap() .from_local_datetime(&NaiveDateTime::new( diff --git a/core/src/value/primitive.rs b/core/src/value/primitive.rs index 8cc317fa9..234be4188 100644 --- a/core/src/value/primitive.rs +++ b/core/src/value/primitive.rs @@ -2667,7 +2667,7 @@ impl PrimitiveValue { /// # use smallvec::smallvec; /// # use chrono::{DateTime, FixedOffset, TimeZone, NaiveDateTime, NaiveDate, NaiveTime}; /// # use std::error::Error; - /// use dicom_core::value::{DicomDateTime, AsRange, DateTimeRange, PreciseDateTimeResult}; + /// use dicom_core::value::{DicomDateTime, AsRange, DateTimeRange, PreciseDateTime}; /// /// # fn main() -> Result<(), Box> { /// @@ -2676,14 +2676,14 @@ impl PrimitiveValue { /// /// assert_eq!( /// dt_value.earliest()?, - /// PreciseDateTimeResult::Naive(NaiveDateTime::new( + /// PreciseDateTime::Naive(NaiveDateTime::new( /// NaiveDate::from_ymd_opt(2012, 12, 21).unwrap(), /// NaiveTime::from_hms_micro_opt(9, 30, 1, 100_000).unwrap() /// )) /// ); /// assert_eq!( /// dt_value.latest()?, - /// PreciseDateTimeResult::Naive(NaiveDateTime::new( + /// PreciseDateTime::Naive(NaiveDateTime::new( /// NaiveDate::from_ymd_opt(2012, 12, 21).unwrap(), /// NaiveTime::from_hms_micro_opt(9, 30, 1, 199_999).unwrap() /// )) @@ -2698,7 +2698,7 @@ impl PrimitiveValue { /// /// assert_eq!( /// dt_value.exact()?, - /// PreciseDateTimeResult::TimeZone( + /// PreciseDateTime::TimeZone( /// default_offset /// .ymd_opt(2012, 12, 21).unwrap() /// .and_hms_micro_opt(9, 30, 1, 123_456).unwrap() @@ -2980,7 +2980,7 @@ impl PrimitiveValue { /// # use dicom_core::value::{C, PrimitiveValue}; /// use chrono::{DateTime, NaiveDate, NaiveTime, NaiveDateTime, FixedOffset, TimeZone, Local}; /// # use std::error::Error; - /// use dicom_core::value::{DateTimeRange, PreciseDateTimeResult}; + /// use dicom_core::value::{DateTimeRange, PreciseDateTime}; /// /// # fn main() -> Result<(), Box> { /// @@ -2992,7 +2992,7 @@ impl PrimitiveValue { /// // lower bound of range is parsed into a PreciseDateTimeResult::TimeZone variant /// assert_eq!( /// dt_range.start(), - /// Some(PreciseDateTimeResult::TimeZone( + /// Some(PreciseDateTime::TimeZone( /// FixedOffset::east_opt(5*3600).unwrap().ymd_opt(1992, 1, 1).unwrap() /// .and_hms_micro_opt(15, 30, 20, 123_000).unwrap() /// ) @@ -3002,7 +3002,7 @@ impl PrimitiveValue { /// // upper bound of range is parsed into a PreciseDateTimeResult::TimeZone variant /// assert_eq!( /// dt_range.end(), - /// Some(PreciseDateTimeResult::TimeZone( + /// Some(PreciseDateTime::TimeZone( /// FixedOffset::east_opt(3*3600).unwrap().ymd_opt(1993, 12, 31).unwrap() /// .and_hms_micro_opt(23, 59, 59, 999_999).unwrap() /// ) diff --git a/core/src/value/range.rs b/core/src/value/range.rs index e497a9348..cbfaa80b0 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -313,7 +313,7 @@ impl AsRange for DicomTime { } impl AsRange for DicomDateTime { - type PreciseValue = PreciseDateTimeResult; + type PreciseValue = PreciseDateTime; type Range = DateTimeRange; fn is_precise(&self) -> bool { @@ -335,7 +335,7 @@ impl AsRange for DicomDateTime { }; match self.time_zone() { - Some(offset) => Ok(PreciseDateTimeResult::TimeZone( + Some(offset) => Ok(PreciseDateTime::TimeZone( offset .from_local_datetime(&NaiveDateTime::new(date, time)) .single() @@ -344,7 +344,7 @@ impl AsRange for DicomDateTime { offset: *offset, })?, )), - None => Ok(PreciseDateTimeResult::Naive(NaiveDateTime::new(date, time))), + None => Ok(PreciseDateTime::Naive(NaiveDateTime::new(date, time))), } } @@ -363,7 +363,7 @@ impl AsRange for DicomDateTime { }; match self.time_zone() { - Some(offset) => Ok(PreciseDateTimeResult::TimeZone( + Some(offset) => Ok(PreciseDateTime::TimeZone( offset .from_local_datetime(&NaiveDateTime::new(date, time)) .single() @@ -372,7 +372,7 @@ impl AsRange for DicomDateTime { offset: *offset, })?, )), - None => Ok(PreciseDateTimeResult::Naive(NaiveDateTime::new(date, time))), + None => Ok(PreciseDateTime::Naive(NaiveDateTime::new(date, time))), } } fn range(&self) -> Result { @@ -380,10 +380,10 @@ impl AsRange for DicomDateTime { let end = self.latest()?; match (start, end) { - (PreciseDateTimeResult::Naive(start), PreciseDateTimeResult::Naive(end)) => { + (PreciseDateTime::Naive(start), PreciseDateTime::Naive(end)) => { DateTimeRange::from_start_to_end(start, end) } - (PreciseDateTimeResult::TimeZone(start), PreciseDateTimeResult::TimeZone(end)) => { + (PreciseDateTime::TimeZone(start), PreciseDateTime::TimeZone(end)) => { DateTimeRange::from_start_to_end_with_time_zone(start, end) } @@ -415,10 +415,10 @@ impl DicomTime { } impl DicomDateTime { - /// Retrieves a [PreciseDateTimeResult] from a date-time value. + /// Retrieves a [PreciseDateTime] from a date-time value. /// If the date-time value is not precise or the conversion leads to ambiguous results, /// it fails. - pub fn to_precise_datetime(&self) -> Result { + pub fn to_precise_datetime(&self) -> Result { self.exact() } @@ -463,7 +463,7 @@ pub struct TimeRange { end: Option, } /// Represents a date-time range, that can either be time-zone naive or time-zone aware. It is stored as two [`Option>`] or -/// two [`Option>`] values. +/// two [`Option`] values. /// [None] means no upper or no lower bound for range is present. /// /// # Example @@ -508,49 +508,49 @@ pub enum DateTimeRange { /// A precise date-time value, that can either be time-zone aware or time-zone naive. /// #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd)] -pub enum PreciseDateTimeResult { +pub enum PreciseDateTime { Naive(NaiveDateTime), TimeZone(DateTime), } -impl PreciseDateTimeResult { +impl PreciseDateTime { /// Retrieves a reference to a `chrono::DateTime` if the result is time-zone aware. pub fn as_datetime_with_time_zone(&self) -> Option<&DateTime> { match self { - PreciseDateTimeResult::Naive(..) => None, - PreciseDateTimeResult::TimeZone(value) => Some(value), + PreciseDateTime::Naive(..) => None, + PreciseDateTime::TimeZone(value) => Some(value), } } /// Retrieves a reference to a `chrono::NaiveDateTime` if the result is time-zone naive. pub fn as_datetime(&self) -> Option<&NaiveDateTime> { match self { - PreciseDateTimeResult::Naive(value) => Some(value), - PreciseDateTimeResult::TimeZone(..) => None, + PreciseDateTime::Naive(value) => Some(value), + PreciseDateTime::TimeZone(..) => None, } } /// Moves out a `chrono::DateTime` if the result is time-zone aware. pub fn into_datetime_with_time_zone(self) -> Option> { match self { - PreciseDateTimeResult::Naive(..) => None, - PreciseDateTimeResult::TimeZone(value) => Some(value), + PreciseDateTime::Naive(..) => None, + PreciseDateTime::TimeZone(value) => Some(value), } } /// Moves out a `chrono::NaiveDateTime` if the result is time-zone naive. pub fn into_datetime(self) -> Option { match self { - PreciseDateTimeResult::Naive(value) => Some(value), - PreciseDateTimeResult::TimeZone(..) => None, + PreciseDateTime::Naive(value) => Some(value), + PreciseDateTime::TimeZone(..) => None, } } /// Returns true if result is time-zone aware. pub fn has_time_zone(&self) -> bool { match self { - PreciseDateTimeResult::Naive(..) => false, - PreciseDateTimeResult::TimeZone(..) => true, + PreciseDateTime::Naive(..) => false, + PreciseDateTime::TimeZone(..) => true, } } } @@ -720,18 +720,18 @@ impl DateTimeRange { } /// Returns the lower bound of the range, if present. - pub fn start(&self) -> Option { + pub fn start(&self) -> Option { match self { - DateTimeRange::Naive { start, .. } => start.map(PreciseDateTimeResult::Naive), - DateTimeRange::TimeZone { start, .. } => start.map(PreciseDateTimeResult::TimeZone), + DateTimeRange::Naive { start, .. } => start.map(PreciseDateTime::Naive), + DateTimeRange::TimeZone { start, .. } => start.map(PreciseDateTime::TimeZone), } } /// Returns the upper bound of the range, if present. - pub fn end(&self) -> Option { + pub fn end(&self) -> Option { match self { - DateTimeRange::Naive { start: _, end } => end.map(PreciseDateTimeResult::Naive), - DateTimeRange::TimeZone { start: _, end } => end.map(PreciseDateTimeResult::TimeZone), + DateTimeRange::Naive { start: _, end } => end.map(PreciseDateTime::Naive), + DateTimeRange::TimeZone { start: _, end } => end.map(PreciseDateTime::TimeZone), } } @@ -876,7 +876,7 @@ pub trait AmbiguousDtRangeParser { /// Because "A Date Time value without the optional suffix is interpreted to be in the local time zone /// of the application creating the Data Element, unless explicitly specified by the Timezone Offset From UTC (0008,0201).", /// this is the default behavior of the parser. -/// https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html +/// #[derive(Debug)] pub struct ToLocalTimeZone; @@ -1077,7 +1077,7 @@ impl AmbiguousDtRangeParser for IgnoreTimeZone { /// Because "A Date Time value without the optional suffix is interpreted to be in the local time zone /// of the application creating the Data Element, unless explicitly specified by the Timezone Offset From UTC (0008,0201).", /// this is the default behavior of the parser. -/// https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html +/// /// /// To customize this behavior, please use [parse_datetime_range_custom()]. /// @@ -1108,8 +1108,8 @@ pub fn parse_datetime_range_impl(buf: &[u8]) -> Resul // starting with separator, range is None-Some let buf = &buf[1..]; match parse_datetime_partial(buf).context(ParseSnafu)?.latest()? { - PreciseDateTimeResult::Naive(end) => Ok(DateTimeRange::from_end(end)), - PreciseDateTimeResult::TimeZone(end_tz) => { + PreciseDateTime::Naive(end) => Ok(DateTimeRange::from_end(end)), + PreciseDateTime::TimeZone(end_tz) => { Ok(DateTimeRange::from_end_with_time_zone(end_tz)) } } @@ -1120,8 +1120,8 @@ pub fn parse_datetime_range_impl(buf: &[u8]) -> Resul .context(ParseSnafu)? .earliest()? { - PreciseDateTimeResult::Naive(start) => Ok(DateTimeRange::from_start(start)), - PreciseDateTimeResult::TimeZone(start_tz) => { + PreciseDateTime::Naive(start) => Ok(DateTimeRange::from_start(start)), + PreciseDateTime::TimeZone(start_tz) => { Ok(DateTimeRange::from_start_with_time_zone(start_tz)) } } @@ -1151,22 +1151,22 @@ pub fn parse_datetime_range_impl(buf: &[u8]) -> Resul //create a result here, to check for range inversion let dtr = match (s.earliest()?, e.latest()?) { ( - PreciseDateTimeResult::Naive(start), - PreciseDateTimeResult::Naive(end), + PreciseDateTime::Naive(start), + PreciseDateTime::Naive(end), ) => DateTimeRange::from_start_to_end(start, end), ( - PreciseDateTimeResult::TimeZone(start), - PreciseDateTimeResult::TimeZone(end), + PreciseDateTime::TimeZone(start), + PreciseDateTime::TimeZone(end), ) => DateTimeRange::from_start_to_end_with_time_zone(start, end), ( // lower bound time-zone was missing - PreciseDateTimeResult::Naive(start), - PreciseDateTimeResult::TimeZone(end), + PreciseDateTime::Naive(start), + PreciseDateTime::TimeZone(end), ) => T::parse_with_ambiguous_start(start, end), ( - PreciseDateTimeResult::TimeZone(start), + PreciseDateTime::TimeZone(start), // upper bound time-zone was missing - PreciseDateTimeResult::Naive(end), + PreciseDateTime::Naive(end), ) => T::parse_with_ambiguous_end(start, end), }; match dtr { @@ -1190,18 +1190,18 @@ pub fn parse_datetime_range_impl(buf: &[u8]) -> Resul .earliest()?, parse_datetime_partial(end).context(ParseSnafu)?.latest()?, ) { - (PreciseDateTimeResult::Naive(start), PreciseDateTimeResult::Naive(end)) => { + (PreciseDateTime::Naive(start), PreciseDateTime::Naive(end)) => { DateTimeRange::from_start_to_end(start, end) } - (PreciseDateTimeResult::TimeZone(start), PreciseDateTimeResult::TimeZone(end)) => { + (PreciseDateTime::TimeZone(start), PreciseDateTime::TimeZone(end)) => { DateTimeRange::from_start_to_end_with_time_zone(start, end) } // lower bound time-zone was missing - (PreciseDateTimeResult::Naive(start), PreciseDateTimeResult::TimeZone(end)) => { + (PreciseDateTime::Naive(start), PreciseDateTime::TimeZone(end)) => { T::parse_with_ambiguous_start(start, end) } // upper bound time-zone was missing - (PreciseDateTimeResult::TimeZone(start), PreciseDateTimeResult::Naive(end)) => { + (PreciseDateTime::TimeZone(start), PreciseDateTime::Naive(end)) => { T::parse_with_ambiguous_end(start, end) } } @@ -1302,7 +1302,7 @@ mod tests { .unwrap() ) .start(), - Some(PreciseDateTimeResult::TimeZone( + Some(PreciseDateTime::TimeZone( offset .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), @@ -1321,7 +1321,7 @@ mod tests { .unwrap() ) .end(), - Some(PreciseDateTimeResult::TimeZone( + Some(PreciseDateTime::TimeZone( offset .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), @@ -1347,7 +1347,7 @@ mod tests { ) .unwrap() .start(), - Some(PreciseDateTimeResult::TimeZone( + Some(PreciseDateTime::TimeZone( offset .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), @@ -1373,7 +1373,7 @@ mod tests { ) .unwrap() .end(), - Some(PreciseDateTimeResult::TimeZone( + Some(PreciseDateTime::TimeZone( offset .from_local_datetime(&NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), @@ -1413,7 +1413,7 @@ mod tests { NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap() )) .start(), - Some(PreciseDateTimeResult::Naive(NaiveDateTime::new( + Some(PreciseDateTime::Naive(NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap() ))) @@ -1424,7 +1424,7 @@ mod tests { NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap() )) .end(), - Some(PreciseDateTimeResult::Naive(NaiveDateTime::new( + Some(PreciseDateTime::Naive(NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap() ))) @@ -1442,7 +1442,7 @@ mod tests { ) .unwrap() .start(), - Some(PreciseDateTimeResult::Naive(NaiveDateTime::new( + Some(PreciseDateTime::Naive(NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), NaiveTime::from_hms_micro_opt(1, 1, 1, 1).unwrap() ))) @@ -1460,7 +1460,7 @@ mod tests { ) .unwrap() .end(), - Some(PreciseDateTimeResult::Naive(NaiveDateTime::new( + Some(PreciseDateTime::Naive(NaiveDateTime::new( NaiveDate::from_ymd_opt(1990, 1, 1).unwrap(), NaiveTime::from_hms_micro_opt(1, 1, 1, 5).unwrap() ))) diff --git a/object/src/mem.rs b/object/src/mem.rs index 1528cf97b..fc32f6404 100644 --- a/object/src/mem.rs +++ b/object/src/mem.rs @@ -889,7 +889,7 @@ where AttributeSelectorStep::Nested { tag, item } => { let e = obj .entries - .get(&tag) + .get(tag) .with_context(|| crate::MissingSequenceSnafu { selector: selector.clone(), step_index: i as u32, From a4970c4b6e7e66b8fc842f1f834db18ad1c8ef8c Mon Sep 17 00:00:00 2001 From: Eduardo Pinho Date: Fri, 1 Mar 2024 18:00:47 +0000 Subject: [PATCH 16/23] Update core/src/value/mod.rs --- core/src/value/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/value/mod.rs b/core/src/value/mod.rs index a7f849b3a..7bc99318d 100644 --- a/core/src/value/mod.rs +++ b/core/src/value/mod.rs @@ -662,8 +662,6 @@ where } /// Retrieves the primitive value as a [`PersonName`]. - /// - /// [1]: super::value::person_name::PersonName pub fn to_person_name(&self) -> Result, ConvertValueError> { match self { Value::Primitive(v) => v.to_person_name(), From 3253ce2527d3b0927c5998c8734d2309922d4bfd Mon Sep 17 00:00:00 2001 From: Eduardo Pinho Date: Sat, 2 Mar 2024 15:20:56 +0000 Subject: [PATCH 17/23] [core] Improve PreciseDateTime - clarify its purpose in the documentation - document variants - rename existing methods and add more methods --- core/src/value/range.rs | 58 ++++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/core/src/value/range.rs b/core/src/value/range.rs index cbfaa80b0..8d6a85c37 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -505,54 +505,86 @@ pub enum DateTimeRange { }, } -/// A precise date-time value, that can either be time-zone aware or time-zone naive. +/// An encapsulated date-time value which is precise to the microsecond +/// and can either be time-zone aware or time-zone naive. /// +/// It is usually the outcome of converting a precise +/// [DICOM date-time value](DicomDateTime) +/// to a [chrono] date-time value. #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd)] pub enum PreciseDateTime { + /// Naive date-time, with no time zone Naive(NaiveDateTime), + /// Date-time with a time zone defined by a fixed offset TimeZone(DateTime), } impl PreciseDateTime { - /// Retrieves a reference to a `chrono::DateTime` if the result is time-zone aware. - pub fn as_datetime_with_time_zone(&self) -> Option<&DateTime> { + /// Retrieves a reference to a [`chrono::DateTime`][chrono::DateTime] + /// if the result is time-zone aware. + pub fn as_datetime(&self) -> Option<&DateTime> { match self { PreciseDateTime::Naive(..) => None, PreciseDateTime::TimeZone(value) => Some(value), } } - /// Retrieves a reference to a `chrono::NaiveDateTime` if the result is time-zone naive. - pub fn as_datetime(&self) -> Option<&NaiveDateTime> { + /// Retrieves a reference to a [`chrono::NaiveDateTime`] + /// only if the result is time-zone naive. + pub fn as_naive_datetime(&self) -> Option<&NaiveDateTime> { match self { PreciseDateTime::Naive(value) => Some(value), PreciseDateTime::TimeZone(..) => None, } } - /// Moves out a `chrono::DateTime` if the result is time-zone aware. - pub fn into_datetime_with_time_zone(self) -> Option> { + /// Moves out a [`chrono::DateTime`](chrono::DateTime) + /// if the result is time-zone aware. + pub fn into_datetime(self) -> Option> { match self { PreciseDateTime::Naive(..) => None, PreciseDateTime::TimeZone(value) => Some(value), } } - /// Moves out a `chrono::NaiveDateTime` if the result is time-zone naive. - pub fn into_datetime(self) -> Option { + /// Moves out a [`chrono::NaiveDateTime`] + /// only if the result is time-zone naive. + pub fn into_naive_datetime(self) -> Option { match self { PreciseDateTime::Naive(value) => Some(value), PreciseDateTime::TimeZone(..) => None, } } - /// Returns true if result is time-zone aware. - pub fn has_time_zone(&self) -> bool { + /// Retrieves the time-zone naive date component + /// of the precise date-time value. + /// + /// # Panics + /// + /// The time-zone aware variant uses `DateTime`, + /// which internally stores the date and time in UTC with a `NaiveDateTime`. + /// This method will panic if the offset from UTC would push the local date + /// outside of the representable range of a `NaiveDate`. + pub fn to_naive_date(&self) -> NaiveDate { + match self { + PreciseDateTime::Naive(value) => value.date(), + PreciseDateTime::TimeZone(value) => value.date_naive(), + } + } + + /// Retrieves the time component of the precise date-time value. + pub fn to_naive_time(&self) -> NaiveTime { match self { - PreciseDateTime::Naive(..) => false, - PreciseDateTime::TimeZone(..) => true, + PreciseDateTime::Naive(value) => value.time(), + PreciseDateTime::TimeZone(value) => value.time(), } } + + /// Returns `true` if the result is time-zone aware. + #[inline] + pub fn has_time_zone(&self) -> bool { + matches!(self, PreciseDateTime::TimeZone(..)) + } } impl DateRange { From aeea024b0c002a760f12f5534d7bd9e7ea25ccab Mon Sep 17 00:00:00 2001 From: Eduardo Pinho Date: Sat, 2 Mar 2024 15:34:30 +0000 Subject: [PATCH 18/23] [core] custom impl PartialOrd for PreciseDateTime - do not provide an order between non-matching variants --- core/src/value/range.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/core/src/value/range.rs b/core/src/value/range.rs index 8d6a85c37..039410fd6 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -511,7 +511,7 @@ pub enum DateTimeRange { /// It is usually the outcome of converting a precise /// [DICOM date-time value](DicomDateTime) /// to a [chrono] date-time value. -#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd)] +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] pub enum PreciseDateTime { /// Naive date-time, with no time zone Naive(NaiveDateTime), @@ -587,6 +587,23 @@ impl PreciseDateTime { } } +/// The partial ordering for `PreciseDateTime` +/// is defined by the partial ordering of matching variants +/// (`Naive` with `Naive`, `TimeZone` with `TimeZone`). +/// +/// Any other comparison cannot be defined, +/// and therefore will always return `None`. +impl PartialOrd for PreciseDateTime { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (PreciseDateTime::Naive(a), PreciseDateTime::Naive(b)) => a.partial_cmp(b), + (PreciseDateTime::TimeZone(a), PreciseDateTime::TimeZone(b)) => a.partial_cmp(b), + _ => None, + } + + } +} + impl DateRange { /// Constructs a new `DateRange` from two `chrono::NaiveDate` values /// monotonically ordered in time. From b62e079045e3f694834005a756d53b242ce4ca89 Mon Sep 17 00:00:00 2001 From: Eduardo Pinho Date: Sat, 2 Mar 2024 15:38:17 +0000 Subject: [PATCH 19/23] [core] Fix doctest in PrimitiveValue --- core/src/value/primitive.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/value/primitive.rs b/core/src/value/primitive.rs index 234be4188..723101fd8 100644 --- a/core/src/value/primitive.rs +++ b/core/src/value/primitive.rs @@ -3102,10 +3102,10 @@ impl PrimitiveValue { /// // values are in the same time-zone /// assert_eq!( /// dt_range.start().unwrap() - /// .as_datetime_with_time_zone().unwrap() + /// .as_datetime().unwrap() /// .offset(), /// dt_range.end().unwrap() - /// .as_datetime_with_time_zone().unwrap() + /// .as_datetime().unwrap() /// .offset() /// ); /// From a77c0cf5c87750f6552a2f9a849d35c00d076a27 Mon Sep 17 00:00:00 2001 From: Eduardo Pinho Date: Sat, 2 Mar 2024 15:39:45 +0000 Subject: [PATCH 20/23] [core] move PreciseDateTime to partial module --- core/src/value/mod.rs | 4 +- core/src/value/partial.rs | 106 ++++++++++++++++++++++++++++++++++++-- core/src/value/range.rs | 101 +----------------------------------- 3 files changed, 106 insertions(+), 105 deletions(-) diff --git a/core/src/value/mod.rs b/core/src/value/mod.rs index 7bc99318d..8423b4dc2 100644 --- a/core/src/value/mod.rs +++ b/core/src/value/mod.rs @@ -14,9 +14,9 @@ pub mod range; pub mod serialize; pub use self::deserialize::Error as DeserializeError; -pub use self::partial::{DicomDate, DicomDateTime, DicomTime}; +pub use self::partial::{DicomDate, DicomDateTime, DicomTime, PreciseDateTime}; pub use self::person_name::PersonName; -pub use self::range::{AsRange, DateRange, DateTimeRange, TimeRange, PreciseDateTime}; +pub use self::range::{AsRange, DateRange, DateTimeRange, TimeRange}; pub use self::primitive::{ CastValueError, ConvertValueError, InvalidValueReadError, ModifyValueError, PrimitiveValue, diff --git a/core/src/value/partial.rs b/core/src/value/partial.rs index bb12f01a7..c4c9fd800 100644 --- a/core/src/value/partial.rs +++ b/core/src/value/partial.rs @@ -189,8 +189,9 @@ enum DicomTimeImpl { /// `DicomDateTime` is always internally represented by a [DicomDate]. /// The [DicomTime] and a timezone [FixedOffset] values are optional. /// -/// It implements [AsRange] trait, which serves to retrieve a [PreciseDateTime](crate::value::range::PreciseDateTime) from values -/// with missing components. +/// It implements [AsRange] trait, +/// which serves to retrieve a [`PreciseDateTime`] +/// from values with missing components. /// # Example /// ``` /// # use std::error::Error; @@ -877,9 +878,108 @@ impl DicomDateTime { } } +/// An encapsulated date-time value which is precise to the microsecond +/// and can either be time-zone aware or time-zone naive. +/// +/// It is usually the outcome of converting a precise +/// [DICOM date-time value](DicomDateTime) +/// to a [chrono] date-time value. +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] +pub enum PreciseDateTime { + /// Naive date-time, with no time zone + Naive(NaiveDateTime), + /// Date-time with a time zone defined by a fixed offset + TimeZone(DateTime), +} + +impl PreciseDateTime { + /// Retrieves a reference to a [`chrono::DateTime`][chrono::DateTime] + /// if the result is time-zone aware. + pub fn as_datetime(&self) -> Option<&DateTime> { + match self { + PreciseDateTime::Naive(..) => None, + PreciseDateTime::TimeZone(value) => Some(value), + } + } + + /// Retrieves a reference to a [`chrono::NaiveDateTime`] + /// only if the result is time-zone naive. + pub fn as_naive_datetime(&self) -> Option<&NaiveDateTime> { + match self { + PreciseDateTime::Naive(value) => Some(value), + PreciseDateTime::TimeZone(..) => None, + } + } + + /// Moves out a [`chrono::DateTime`](chrono::DateTime) + /// if the result is time-zone aware. + pub fn into_datetime(self) -> Option> { + match self { + PreciseDateTime::Naive(..) => None, + PreciseDateTime::TimeZone(value) => Some(value), + } + } + + /// Moves out a [`chrono::NaiveDateTime`] + /// only if the result is time-zone naive. + pub fn into_naive_datetime(self) -> Option { + match self { + PreciseDateTime::Naive(value) => Some(value), + PreciseDateTime::TimeZone(..) => None, + } + } + + /// Retrieves the time-zone naive date component + /// of the precise date-time value. + /// + /// # Panics + /// + /// The time-zone aware variant uses `DateTime`, + /// which internally stores the date and time in UTC with a `NaiveDateTime`. + /// This method will panic if the offset from UTC would push the local date + /// outside of the representable range of a `NaiveDate`. + pub fn to_naive_date(&self) -> NaiveDate { + match self { + PreciseDateTime::Naive(value) => value.date(), + PreciseDateTime::TimeZone(value) => value.date_naive(), + } + } + + /// Retrieves the time component of the precise date-time value. + pub fn to_naive_time(&self) -> NaiveTime { + match self { + PreciseDateTime::Naive(value) => value.time(), + PreciseDateTime::TimeZone(value) => value.time(), + } + } + + /// Returns `true` if the result is time-zone aware. + #[inline] + pub fn has_time_zone(&self) -> bool { + matches!(self, PreciseDateTime::TimeZone(..)) + } +} + +/// The partial ordering for `PreciseDateTime` +/// is defined by the partial ordering of matching variants +/// (`Naive` with `Naive`, `TimeZone` with `TimeZone`). +/// +/// Any other comparison cannot be defined, +/// and therefore will always return `None`. +impl PartialOrd for PreciseDateTime { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (PreciseDateTime::Naive(a), PreciseDateTime::Naive(b)) => a.partial_cmp(b), + (PreciseDateTime::TimeZone(a), PreciseDateTime::TimeZone(b)) => a.partial_cmp(b), + _ => None, + } + + } +} + #[cfg(test)] mod tests { - use crate::value::range::{AsRange, PreciseDateTime}; + use crate::value::range::AsRange; use super::*; use chrono::{NaiveDateTime, TimeZone}; diff --git a/core/src/value/range.rs b/core/src/value/range.rs index 039410fd6..1eb283839 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -9,7 +9,7 @@ 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}; +use crate::value::partial::{DicomDate, DicomDateTime, DicomTime, PreciseDateTime}; #[derive(Debug, Snafu)] #[non_exhaustive] @@ -505,105 +505,6 @@ pub enum DateTimeRange { }, } -/// An encapsulated date-time value which is precise to the microsecond -/// and can either be time-zone aware or time-zone naive. -/// -/// It is usually the outcome of converting a precise -/// [DICOM date-time value](DicomDateTime) -/// to a [chrono] date-time value. -#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] -pub enum PreciseDateTime { - /// Naive date-time, with no time zone - Naive(NaiveDateTime), - /// Date-time with a time zone defined by a fixed offset - TimeZone(DateTime), -} - -impl PreciseDateTime { - /// Retrieves a reference to a [`chrono::DateTime`][chrono::DateTime] - /// if the result is time-zone aware. - pub fn as_datetime(&self) -> Option<&DateTime> { - match self { - PreciseDateTime::Naive(..) => None, - PreciseDateTime::TimeZone(value) => Some(value), - } - } - - /// Retrieves a reference to a [`chrono::NaiveDateTime`] - /// only if the result is time-zone naive. - pub fn as_naive_datetime(&self) -> Option<&NaiveDateTime> { - match self { - PreciseDateTime::Naive(value) => Some(value), - PreciseDateTime::TimeZone(..) => None, - } - } - - /// Moves out a [`chrono::DateTime`](chrono::DateTime) - /// if the result is time-zone aware. - pub fn into_datetime(self) -> Option> { - match self { - PreciseDateTime::Naive(..) => None, - PreciseDateTime::TimeZone(value) => Some(value), - } - } - - /// Moves out a [`chrono::NaiveDateTime`] - /// only if the result is time-zone naive. - pub fn into_naive_datetime(self) -> Option { - match self { - PreciseDateTime::Naive(value) => Some(value), - PreciseDateTime::TimeZone(..) => None, - } - } - - /// Retrieves the time-zone naive date component - /// of the precise date-time value. - /// - /// # Panics - /// - /// The time-zone aware variant uses `DateTime`, - /// which internally stores the date and time in UTC with a `NaiveDateTime`. - /// This method will panic if the offset from UTC would push the local date - /// outside of the representable range of a `NaiveDate`. - pub fn to_naive_date(&self) -> NaiveDate { - match self { - PreciseDateTime::Naive(value) => value.date(), - PreciseDateTime::TimeZone(value) => value.date_naive(), - } - } - - /// Retrieves the time component of the precise date-time value. - pub fn to_naive_time(&self) -> NaiveTime { - match self { - PreciseDateTime::Naive(value) => value.time(), - PreciseDateTime::TimeZone(value) => value.time(), - } - } - - /// Returns `true` if the result is time-zone aware. - #[inline] - pub fn has_time_zone(&self) -> bool { - matches!(self, PreciseDateTime::TimeZone(..)) - } -} - -/// The partial ordering for `PreciseDateTime` -/// is defined by the partial ordering of matching variants -/// (`Naive` with `Naive`, `TimeZone` with `TimeZone`). -/// -/// Any other comparison cannot be defined, -/// and therefore will always return `None`. -impl PartialOrd for PreciseDateTime { - fn partial_cmp(&self, other: &Self) -> Option { - match (self, other) { - (PreciseDateTime::Naive(a), PreciseDateTime::Naive(b)) => a.partial_cmp(b), - (PreciseDateTime::TimeZone(a), PreciseDateTime::TimeZone(b)) => a.partial_cmp(b), - _ => None, - } - - } -} - impl DateRange { /// Constructs a new `DateRange` from two `chrono::NaiveDate` values /// monotonically ordered in time. From b6b794d4b98f80e925c42dd29acf48dda3dd794b Mon Sep 17 00:00:00 2001 From: Eduardo Pinho Date: Sat, 2 Mar 2024 15:45:05 +0000 Subject: [PATCH 21/23] [core] Improve docs of parse_datetime and parse_datetime_partial --- core/src/value/deserialize.rs | 47 ++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/core/src/value/deserialize.rs b/core/src/value/deserialize.rs index 4d258b1e3..476c9c279 100644 --- a/core/src/value/deserialize.rs +++ b/core/src/value/deserialize.rs @@ -330,14 +330,15 @@ where }) } -/** Retrieve a `chrono::DateTime` from the given text, while assuming the given UTC offset. -* If a date/time component is missing, the operation fails. -* Presence of the second fraction component `.FFFFFF` is mandatory with at - least one digit accuracy `.F` while missing digits default to zero. -* For DateTime with missing components, or if exact second fraction accuracy needs to be preserved, - use `parse_datetime_partial`. -*/ -#[deprecated(since = "0.7.0", note = "Only use parse_datetime_partial()")] +/// Retrieve a `chrono::DateTime` from the given text, while assuming the given UTC offset. +/// +/// If a date/time component is missing, the operation fails. +/// Presence of the second fraction component `.FFFFFF` is mandatory with at +/// least one digit accuracy `.F` while missing digits default to zero. +/// +/// [`parse_datetime_partial`] should be preferred, +/// because it is more flexible and resilient to missing components. +#[deprecated(since = "0.7.0", note = "Use `parse_datetime_partial()` then `to_precise_datetime()`")] pub fn parse_datetime(buf: &[u8], dt_utc_offset: FixedOffset) -> Result> { let date = parse_date(buf)?; let buf = &buf[8..]; @@ -375,10 +376,32 @@ pub fn parse_datetime(buf: &[u8], dt_utc_offset: FixedOffset) -> Result>(()) +/// ``` pub fn parse_datetime_partial(buf: &[u8]) -> Result { let (date, rest) = parse_date_partial(buf)?; From 406bd284c44d65ba0bac757124cf69685f77a025 Mon Sep 17 00:00:00 2001 From: Eduardo Pinho Date: Sat, 2 Mar 2024 15:49:36 +0000 Subject: [PATCH 22/23] [core] impl FromStr for DicomDateTime --- core/src/value/deserialize.rs | 5 +++++ core/src/value/partial.rs | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/core/src/value/deserialize.rs b/core/src/value/deserialize.rs index 476c9c279..38ceb40af 100644 --- a/core/src/value/deserialize.rs +++ b/core/src/value/deserialize.rs @@ -338,6 +338,8 @@ where /// /// [`parse_datetime_partial`] should be preferred, /// because it is more flexible and resilient to missing components. +/// See also the implementation of [`FromStr`](std::str::FromStr) +/// for [`DicomDateTime`]. #[deprecated(since = "0.7.0", note = "Use `parse_datetime_partial()` then `to_precise_datetime()`")] pub fn parse_datetime(buf: &[u8], dt_utc_offset: FixedOffset) -> Result> { let date = parse_date(buf)?; @@ -379,6 +381,9 @@ pub fn parse_datetime(buf: &[u8], dt_utc_offset: FixedOffset) -> Result Result { + crate::value::deserialize::parse_datetime_partial(s.as_bytes()) + } +} + impl DicomDate { /** * Retrieves a dicom encoded string representation of the value. From 0d9df8a5a8e1e665b267554c7aa4213ab1bd85d1 Mon Sep 17 00:00:00 2001 From: Eduardo Pinho Date: Sat, 2 Mar 2024 16:10:56 +0000 Subject: [PATCH 23/23] [core] Improve docs of AmbiguousDtRangeParser and impls --- core/src/value/range.rs | 45 ++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/core/src/value/range.rs b/core/src/value/range.rs index 1eb283839..87bbe9fe1 100644 --- a/core/src/value/range.rs +++ b/core/src/value/range.rs @@ -799,14 +799,20 @@ pub fn parse_time_range(buf: &[u8]) -> Result { } } -/// The Dicom standard allows for parsing a date-time range in which one DT value provides time-zone -/// information but the other does not. +/// The DICOM standard allows for parsing a date-time range +/// in which one DT value provides time-zone information +/// but the other one does not. +/// An example of this is the value `19750101-19800101+0200`. /// -/// Example '19750101-19800101+0200'. +/// In such cases, the missing time-zone can be interpreted as the local time-zone +/// the time-zone provided by the upper bound, or something else altogether. /// -/// In such case, the missing time-zone can be interpreted as the local time-zone or the time-zone -/// provided with the upper bound (or something else altogether). -/// This trait is implemented by parsers handling the afformentioned situation. +/// This trait is implemented by parsers handling the aforementioned situation. +/// For concrete implementations, see: +/// - [`ToLocalTimeZone`] (the default implementation) +/// - [`ToKnownTimeZone`] +/// - [`FailOnAmbiguousRange`] +/// - [`IgnoreTimeZone`] pub trait AmbiguousDtRangeParser { /// Retrieve a [DateTimeRange] if the lower range bound is missing a time-zone fn parse_with_ambiguous_start( @@ -820,13 +826,17 @@ pub trait AmbiguousDtRangeParser { ) -> Result; } -/// For the missing time-zone use time-zone information of the local system clock. +/// For the missing time-zone, +/// use time-zone information of the local system clock. /// Retrieves a [DateTimeRange::TimeZone]. /// -/// Because "A Date Time value without the optional suffix is interpreted to be in the local time zone -/// of the application creating the Data Element, unless explicitly specified by the Timezone Offset From UTC (0008,0201).", -/// this is the default behavior of the parser. -/// +/// This is the default behavior of the parser, +/// which helps attain compliance with the standard +/// as per [DICOM PS3.5 6.2](https://dicom.nema.org/medical/dicom/2023e/output/chtml/part05/sect_6.2.html): +/// +/// > A Date Time Value without the optional suffix +/// > is interpreted to be in the local time zone of the application creating the Data Element, +/// > unless explicitly specified by the Timezone Offset From UTC (0008,0201). #[derive(Debug)] pub struct ToLocalTimeZone; @@ -835,7 +845,7 @@ pub struct ToLocalTimeZone; #[derive(Debug)] pub struct ToKnownTimeZone; -/// Fail if ambiguous date-time range is parsed +/// Fail on an attempt to parse an ambiguous date-time range. #[derive(Debug)] pub struct FailOnAmbiguousRange; @@ -1024,10 +1034,13 @@ impl AmbiguousDtRangeParser for IgnoreTimeZone { /// If the parser encounters two date-time values, where one is time-zone aware and the other is not, /// it will use the local time-zone offset and use it instead of the missing time-zone. /// -/// Because "A Date Time value without the optional suffix is interpreted to be in the local time zone -/// of the application creating the Data Element, unless explicitly specified by the Timezone Offset From UTC (0008,0201).", -/// this is the default behavior of the parser. -/// +/// This is the default behavior of the parser, +/// which helps attain compliance with the standard +/// as per [DICOM PS3.5 6.2](https://dicom.nema.org/medical/dicom/2023e/output/chtml/part05/sect_6.2.html): +/// +/// > A Date Time Value without the optional suffix +/// > is interpreted to be in the local time zone of the application creating the Data Element, +/// > unless explicitly specified by the Timezone Offset From UTC (0008,0201). /// /// To customize this behavior, please use [parse_datetime_range_custom()]. ///