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;
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum Error {
#[snafu(display("To combine a DicomDate with a DicomTime value, the DicomDate has to be precise. Precision is: '{:?}'", value))]
DateTimeFromPartials {
value: DateComponent,
backtrace: Backtrace,
},
#[snafu(display(
"'{:?}' has invalid value: '{}', must be in {:?}",
component,
value,
range
))]
InvalidComponent {
component: DateComponent,
value: u32,
range: RangeInclusive<u32>,
backtrace: Backtrace,
},
#[snafu(display(
"Second fraction precision '{}' is out of range, must be in 0..=6",
value
))]
FractionPrecisionRange { value: u32, backtrace: Backtrace },
#[snafu(display(
"Number of digits in decimal representation of fraction '{}' does not match it's precision '{}'",
fraction,
precision
))]
FractionPrecisionMismatch {
fraction: u32,
precision: u32,
backtrace: Backtrace,
},
#[snafu(display("Conversion of value '{}' into {:?} failed", value, component))]
Conversion {
value: String,
component: DateComponent,
source: std::num::TryFromIntError,
},
#[snafu(display(
"Cannot convert from an imprecise value. This value represents a date / time range"
))]
ImpreciseValue { backtrace: Backtrace },
}
type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, PartialEq, Copy, Clone, Eq, Hash, PartialOrd, Ord)]
pub enum DateComponent {
Year,
Month,
Day,
Hour,
Minute,
Second,
Millisecond,
Fraction,
UtcWest,
UtcEast,
}
#[derive(Clone, Copy, PartialEq)]
pub struct DicomDate(DicomDateImpl);
#[derive(Clone, Copy, PartialEq)]
pub struct DicomTime(DicomTimeImpl);
#[derive(Debug, Clone, Copy, PartialEq)]
enum DicomDateImpl {
Year(u16),
Month(u16, u8),
Day(u16, u8, u8),
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum DicomTimeImpl {
Hour(u8),
Minute(u8, u8),
Second(u8, u8, u8),
Fraction(u8, u8, u8, u32, u8),
}
#[derive(PartialEq, Clone, Copy)]
pub struct DicomDateTime {
date: DicomDate,
time: Option<DicomTime>,
time_zone: Option<FixedOffset>,
}
pub fn check_component<T>(component: DateComponent, value: &T) -> Result<()>
where
T: Into<u32> + Copy,
{
let range = match component {
DateComponent::Year => 0..=9_999,
DateComponent::Month => 1..=12,
DateComponent::Day => 1..=31,
DateComponent::Hour => 0..=23,
DateComponent::Minute => 0..=59,
DateComponent::Second => 0..=60,
DateComponent::Millisecond => 0..=999,
DateComponent::Fraction => 0..=999_999,
DateComponent::UtcWest => 0..=(12 * 3600),
DateComponent::UtcEast => 0..=(14 * 3600),
};
let value: u32 = (*value).into();
if range.contains(&value) {
Ok(())
} else {
InvalidComponentSnafu {
component,
value,
range,
}
.fail()
}
}
impl DicomDate {
pub fn from_y(year: u16) -> Result<DicomDate> {
check_component(DateComponent::Year, &year)?;
Ok(DicomDate(DicomDateImpl::Year(year)))
}
pub fn from_ym(year: u16, month: u8) -> Result<DicomDate> {
check_component(DateComponent::Year, &year)?;
check_component(DateComponent::Month, &month)?;
Ok(DicomDate(DicomDateImpl::Month(year, month)))
}
pub fn from_ymd(year: u16, month: u8, day: u8) -> Result<DicomDate> {
check_component(DateComponent::Year, &year)?;
check_component(DateComponent::Month, &month)?;
check_component(DateComponent::Day, &day)?;
Ok(DicomDate(DicomDateImpl::Day(year, month, day)))
}
pub fn year(&self) -> &u16 {
match self {
DicomDate(DicomDateImpl::Year(y)) => y,
DicomDate(DicomDateImpl::Month(y, _)) => y,
DicomDate(DicomDateImpl::Day(y, _, _)) => y,
}
}
pub fn month(&self) -> Option<&u8> {
match self {
DicomDate(DicomDateImpl::Year(_)) => None,
DicomDate(DicomDateImpl::Month(_, m)) => Some(m),
DicomDate(DicomDateImpl::Day(_, m, _)) => Some(m),
}
}
pub fn day(&self) -> Option<&u8> {
match self {
DicomDate(DicomDateImpl::Year(_)) => None,
DicomDate(DicomDateImpl::Month(_, _)) => None,
DicomDate(DicomDateImpl::Day(_, _, d)) => Some(d),
}
}
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 {
type Error = Error;
fn try_from(date: &NaiveDate) -> Result<Self> {
let year: u16 = date.year().try_into().with_context(|_| ConversionSnafu {
value: date.year().to_string(),
component: DateComponent::Year,
})?;
let month: u8 = date.month().try_into().with_context(|_| ConversionSnafu {
value: date.month().to_string(),
component: DateComponent::Month,
})?;
let day: u8 = date.day().try_into().with_context(|_| ConversionSnafu {
value: date.day().to_string(),
component: DateComponent::Day,
})?;
DicomDate::from_ymd(year, month, day)
}
}
impl fmt::Display for DicomDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DicomDate(DicomDateImpl::Year(y)) => write!(f, "{:04}", y),
DicomDate(DicomDateImpl::Month(y, m)) => write!(f, "{:04}-{:02}", y, m),
DicomDate(DicomDateImpl::Day(y, m, d)) => write!(f, "{:04}-{:02}-{:02}", y, m, d),
}
}
}
impl fmt::Debug for DicomDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DicomDate(DicomDateImpl::Year(y)) => write!(f, "{:04}-MM-DD", y),
DicomDate(DicomDateImpl::Month(y, m)) => write!(f, "{:04}-{:02}-DD", y, m),
DicomDate(DicomDateImpl::Day(y, m, d)) => write!(f, "{:04}-{:02}-{:02}", y, m, d),
}
}
}
impl DicomTime {
pub fn from_h(hour: u8) -> Result<DicomTime> {
check_component(DateComponent::Hour, &hour)?;
Ok(DicomTime(DicomTimeImpl::Hour(hour)))
}
pub fn from_hm(hour: u8, minute: u8) -> Result<DicomTime> {
check_component(DateComponent::Hour, &hour)?;
check_component(DateComponent::Minute, &minute)?;
Ok(DicomTime(DicomTimeImpl::Minute(hour, minute)))
}
pub fn from_hms(hour: u8, minute: u8, second: u8) -> Result<DicomTime> {
check_component(DateComponent::Hour, &hour)?;
check_component(DateComponent::Minute, &minute)?;
check_component(DateComponent::Second, &second)?;
Ok(DicomTime(DicomTimeImpl::Second(hour, minute, second)))
}
pub fn from_hms_milli(hour: u8, minute: u8, second: u8, millisecond: u32) -> Result<DicomTime> {
check_component(DateComponent::Millisecond, &millisecond)?;
Ok(DicomTime(DicomTimeImpl::Fraction(
hour,
minute,
second,
millisecond,
3,
)))
}
pub fn from_hms_micro(hour: u8, minute: u8, second: u8, microsecond: u32) -> Result<DicomTime> {
check_component(DateComponent::Fraction, µsecond)?;
Ok(DicomTime(DicomTimeImpl::Fraction(
hour,
minute,
second,
microsecond,
6,
)))
}
pub fn hour(&self) -> &u8 {
match self {
DicomTime(DicomTimeImpl::Hour(h)) => h,
DicomTime(DicomTimeImpl::Minute(h, _)) => h,
DicomTime(DicomTimeImpl::Second(h, _, _)) => h,
DicomTime(DicomTimeImpl::Fraction(h, _, _, _, _)) => h,
}
}
pub fn minute(&self) -> Option<&u8> {
match self {
DicomTime(DicomTimeImpl::Hour(_)) => None,
DicomTime(DicomTimeImpl::Minute(_, m)) => Some(m),
DicomTime(DicomTimeImpl::Second(_, m, _)) => Some(m),
DicomTime(DicomTimeImpl::Fraction(_, m, _, _, _)) => Some(m),
}
}
pub fn second(&self) -> Option<&u8> {
match self {
DicomTime(DicomTimeImpl::Hour(_)) => None,
DicomTime(DicomTimeImpl::Minute(_, _)) => None,
DicomTime(DicomTimeImpl::Second(_, _, s)) => Some(s),
DicomTime(DicomTimeImpl::Fraction(_, _, s, _, _)) => Some(s),
}
}
pub fn fraction(&self) -> Option<&u32> {
match self {
DicomTime(DicomTimeImpl::Hour(_)) => None,
DicomTime(DicomTimeImpl::Minute(_, _)) => None,
DicomTime(DicomTimeImpl::Second(_, _, _)) => None,
DicomTime(DicomTimeImpl::Fraction(_, _, _, f, fp)) => match fp {
6 => Some(f),
_ => None,
},
}
}
pub(crate) fn fraction_and_precision(&self) -> Option<(&u32, &u8)> {
match self {
DicomTime(DicomTimeImpl::Hour(_)) => None,
DicomTime(DicomTimeImpl::Minute(_, _)) => None,
DicomTime(DicomTimeImpl::Second(_, _, _)) => None,
DicomTime(DicomTimeImpl::Fraction(_, _, _, f, fp)) => Some((f, fp)),
}
}
pub(crate) fn from_hmsf(
hour: u8,
minute: u8,
second: u8,
fraction: u32,
frac_precision: u8,
) -> Result<DicomTime> {
if !(1..=6).contains(&frac_precision) {
return FractionPrecisionRangeSnafu {
value: frac_precision,
}
.fail();
}
if u32::pow(10, frac_precision as u32) < fraction {
return FractionPrecisionMismatchSnafu {
fraction,
precision: frac_precision,
}
.fail();
}
check_component(DateComponent::Hour, &hour)?;
check_component(DateComponent::Minute, &minute)?;
check_component(DateComponent::Second, &second)?;
let f: u32 = fraction * u32::pow(10, 6 - frac_precision as u32);
check_component(DateComponent::Fraction, &f)?;
Ok(DicomTime(DicomTimeImpl::Fraction(
hour,
minute,
second,
fraction,
frac_precision,
)))
}
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 {
type Error = Error;
fn try_from(time: &NaiveTime) -> Result<Self> {
let hour: u8 = time.hour().try_into().with_context(|_| ConversionSnafu {
value: time.hour().to_string(),
component: DateComponent::Hour,
})?;
let minute: u8 = time.minute().try_into().with_context(|_| ConversionSnafu {
value: time.minute().to_string(),
component: DateComponent::Minute,
})?;
let second: u8 = time.second().try_into().with_context(|_| ConversionSnafu {
value: time.second().to_string(),
component: DateComponent::Second,
})?;
let microsecond = time.nanosecond() / 1000;
let (second, microsecond) = if microsecond >= 1_000_000 && second == 59 {
(60, microsecond - 1_000_000)
} else {
(second, microsecond)
};
DicomTime::from_hms_micro(hour, minute, second, microsecond)
}
}
impl fmt::Display for DicomTime {
fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DicomTime(DicomTimeImpl::Hour(h)) => write!(frm, "{:02}", h),
DicomTime(DicomTimeImpl::Minute(h, m)) => write!(frm, "{:02}:{:02}", h, m),
DicomTime(DicomTimeImpl::Second(h, m, s)) => {
write!(frm, "{:02}:{:02}:{:02}", h, m, s)
}
DicomTime(DicomTimeImpl::Fraction(h, m, s, f, fp)) => {
let sfrac = (u32::pow(10, *fp as u32) + f).to_string();
write!(
frm,
"{:02}:{:02}:{:02}.{}",
h,
m,
s,
match f {
0 => "0",
_ => sfrac.get(1..).unwrap(),
}
)
}
}
}
}
impl fmt::Debug for DicomTime {
fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DicomTime(DicomTimeImpl::Hour(h)) => write!(frm, "{:02}:mm:ss.FFFFFF", h),
DicomTime(DicomTimeImpl::Minute(h, m)) => write!(frm, "{:02}:{:02}:ss.FFFFFF", h, m),
DicomTime(DicomTimeImpl::Second(h, m, s)) => {
write!(frm, "{:02}:{:02}:{:02}.FFFFFF", h, m, s)
}
DicomTime(DicomTimeImpl::Fraction(h, m, s, f, _fp)) => {
write!(frm, "{:02}:{:02}:{:02}.{:F<6}", h, m, s, f)
}
}
}
}
impl DicomDateTime {
pub fn from_date_with_time_zone(date: DicomDate, time_zone: FixedOffset) -> DicomDateTime {
DicomDateTime {
date,
time: None,
time_zone: Some(time_zone),
}
}
pub fn from_date(date: DicomDate) -> DicomDateTime {
DicomDateTime {
date,
time: None,
time_zone: None,
}
}
pub fn from_date_and_time(date: DicomDate, time: DicomTime) -> Result<DicomDateTime> {
if date.is_precise() {
Ok(DicomDateTime {
date,
time: Some(time),
time_zone: None,
})
} else {
DateTimeFromPartialsSnafu {
value: date.precision(),
}
.fail()
}
}
pub fn from_date_and_time_with_time_zone(
date: DicomDate,
time: DicomTime,
time_zone: FixedOffset,
) -> Result<DicomDateTime> {
if date.is_precise() {
Ok(DicomDateTime {
date,
time: Some(time),
time_zone: Some(time_zone),
})
} else {
DateTimeFromPartialsSnafu {
value: date.precision(),
}
.fail()
}
}
pub fn date(&self) -> &DicomDate {
&self.date
}
pub fn time(&self) -> Option<&DicomTime> {
self.time.as_ref()
}
pub fn time_zone(&self) -> Option<&FixedOffset> {
self.time_zone.as_ref()
}
pub fn has_time_zone(&self) -> bool {
self.time_zone.is_some()
}
#[deprecated(since = "0.7.0", note = "Use `time_zone` instead")]
pub fn offset(&self) {}
}
impl TryFrom<&DateTime<FixedOffset>> for DicomDateTime {
type Error = Error;
fn try_from(dt: &DateTime<FixedOffset>) -> Result<Self> {
let year: u16 = dt.year().try_into().with_context(|_| ConversionSnafu {
value: dt.year().to_string(),
component: DateComponent::Year,
})?;
let month: u8 = dt.month().try_into().with_context(|_| ConversionSnafu {
value: dt.month().to_string(),
component: DateComponent::Month,
})?;
let day: u8 = dt.day().try_into().with_context(|_| ConversionSnafu {
value: dt.day().to_string(),
component: DateComponent::Day,
})?;
let hour: u8 = dt.hour().try_into().with_context(|_| ConversionSnafu {
value: dt.hour().to_string(),
component: DateComponent::Hour,
})?;
let minute: u8 = dt.minute().try_into().with_context(|_| ConversionSnafu {
value: dt.minute().to_string(),
component: DateComponent::Minute,
})?;
let second: u8 = dt.second().try_into().with_context(|_| ConversionSnafu {
value: dt.second().to_string(),
component: DateComponent::Second,
})?;
let microsecond = dt.nanosecond() / 1000;
let (second, microsecond) = if microsecond >= 1_000_000 && second == 59 {
(60, microsecond - 1_000_000)
} else {
(second, microsecond)
};
DicomDateTime::from_date_and_time_with_time_zone(
DicomDate::from_ymd(year, month, day)?,
DicomTime::from_hms_micro(hour, minute, second, microsecond)?,
*dt.offset(),
)
}
}
impl TryFrom<&NaiveDateTime> for DicomDateTime {
type Error = Error;
fn try_from(dt: &NaiveDateTime) -> Result<Self> {
let year: u16 = dt.year().try_into().with_context(|_| ConversionSnafu {
value: dt.year().to_string(),
component: DateComponent::Year,
})?;
let month: u8 = dt.month().try_into().with_context(|_| ConversionSnafu {
value: dt.month().to_string(),
component: DateComponent::Month,
})?;
let day: u8 = dt.day().try_into().with_context(|_| ConversionSnafu {
value: dt.day().to_string(),
component: DateComponent::Day,
})?;
let hour: u8 = dt.hour().try_into().with_context(|_| ConversionSnafu {
value: dt.hour().to_string(),
component: DateComponent::Hour,
})?;
let minute: u8 = dt.minute().try_into().with_context(|_| ConversionSnafu {
value: dt.minute().to_string(),
component: DateComponent::Minute,
})?;
let second: u8 = dt.second().try_into().with_context(|_| ConversionSnafu {
value: dt.second().to_string(),
component: DateComponent::Second,
})?;
let microsecond = dt.nanosecond() / 1000;
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 {
match self.time {
None => match self.time_zone {
Some(offset) => write!(frm, "{} {}", self.date, offset),
None => write!(frm, "{}", self.date),
},
Some(time) => match self.time_zone {
Some(offset) => write!(frm, "{} {} {}", self.date, time, offset),
None => write!(frm, "{} {}", self.date, time),
},
}
}
}
impl fmt::Debug for DicomDateTime {
fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.time {
None => match self.time_zone {
Some(offset) => write!(frm, "{:?} {}", self.date, offset),
None => write!(frm, "{:?}", self.date),
},
Some(time) => match self.time_zone {
Some(offset) => write!(frm, "{:?} {:?} {}", self.date, time, offset),
None => write!(frm, "{:?} {:?}", self.date, time),
},
}
}
}
impl std::str::FromStr for DicomDateTime {
type Err = crate::value::DeserializeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
crate::value::deserialize::parse_datetime_partial(s.as_bytes())
}
}
impl DicomDate {
pub fn to_encoded(&self) -> String {
match self {
DicomDate(DicomDateImpl::Year(y)) => format!("{:04}", y),
DicomDate(DicomDateImpl::Month(y, m)) => format!("{:04}{:02}", y, m),
DicomDate(DicomDateImpl::Day(y, m, d)) => format!("{:04}{:02}{:02}", y, m, d),
}
}
}
impl DicomTime {
pub fn to_encoded(&self) -> String {
match self {
DicomTime(DicomTimeImpl::Hour(h)) => format!("{:02}", h),
DicomTime(DicomTimeImpl::Minute(h, m)) => format!("{:02}{:02}", h, m),
DicomTime(DicomTimeImpl::Second(h, m, s)) => format!("{:02}{:02}{:02}", h, m, s),
DicomTime(DicomTimeImpl::Fraction(h, m, s, f, fp)) => {
let sfrac = (u32::pow(10, *fp as u32) + f).to_string();
format!("{:02}{:02}{:02}.{}", h, m, s, sfrac.get(1..).unwrap())
}
}
}
}
impl DicomDateTime {
pub fn to_encoded(&self) -> String {
match self.time {
Some(time) => match self.time_zone {
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.time_zone {
Some(offset) => format!(
"{}{}",
self.date.to_encoded(),
offset.to_string().replace(':', "")
),
None => self.date.to_encoded().to_string(),
},
}
}
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum PreciseDateTime {
Naive(NaiveDateTime),
TimeZone(DateTime<FixedOffset>),
}
impl PreciseDateTime {
pub fn as_datetime(&self) -> Option<&DateTime<FixedOffset>> {
match self {
PreciseDateTime::Naive(..) => None,
PreciseDateTime::TimeZone(value) => Some(value),
}
}
pub fn as_naive_datetime(&self) -> Option<&NaiveDateTime> {
match self {
PreciseDateTime::Naive(value) => Some(value),
PreciseDateTime::TimeZone(..) => None,
}
}
pub fn into_datetime(self) -> Option<DateTime<FixedOffset>> {
match self {
PreciseDateTime::Naive(..) => None,
PreciseDateTime::TimeZone(value) => Some(value),
}
}
pub fn into_naive_datetime(self) -> Option<NaiveDateTime> {
match self {
PreciseDateTime::Naive(value) => Some(value),
PreciseDateTime::TimeZone(..) => None,
}
}
pub fn to_naive_date(&self) -> NaiveDate {
match self {
PreciseDateTime::Naive(value) => value.date(),
PreciseDateTime::TimeZone(value) => value.date_naive(),
}
}
pub fn to_naive_time(&self) -> NaiveTime {
match self {
PreciseDateTime::Naive(value) => value.time(),
PreciseDateTime::TimeZone(value) => value.time(),
}
}
#[inline]
pub fn has_time_zone(&self) -> bool {
matches!(self, PreciseDateTime::TimeZone(..))
}
}
impl PartialOrd for PreciseDateTime {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
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 super::*;
use chrono::TimeZone;
#[test]
fn test_dicom_date() {
assert_eq!(
DicomDate::from_ymd(1944, 2, 29).unwrap(),
DicomDate(DicomDateImpl::Day(1944, 2, 29))
);
assert!(DicomDate::from_ymd(1945, 2, 29).unwrap().is_precise());
assert_eq!(
DicomDate::from_ym(1944, 2).unwrap(),
DicomDate(DicomDateImpl::Month(1944, 2))
);
assert_eq!(
DicomDate::from_y(1944).unwrap(),
DicomDate(DicomDateImpl::Year(1944))
);
assert_eq!(DicomDate::from_ymd(1944, 2, 29).unwrap().is_precise(), true);
assert_eq!(DicomDate::from_ym(1944, 2).unwrap().is_precise(), false);
assert_eq!(DicomDate::from_y(1944).unwrap().is_precise(), false);
assert_eq!(
DicomDate::from_ymd(1944, 2, 29)
.unwrap()
.earliest()
.unwrap(),
NaiveDate::from_ymd_opt(1944, 2, 29).unwrap()
);
assert_eq!(
DicomDate::from_ymd(1944, 2, 29).unwrap().latest().unwrap(),
NaiveDate::from_ymd_opt(1944, 2, 29).unwrap()
);
assert_eq!(
DicomDate::from_y(1944).unwrap().earliest().unwrap(),
NaiveDate::from_ymd_opt(1944, 1, 1).unwrap()
);
assert_eq!(
DicomDate::from_ym(1944, 2).unwrap().latest().unwrap(),
NaiveDate::from_ymd_opt(1944, 2, 29).unwrap()
);
assert_eq!(
DicomDate::from_ym(1945, 2).unwrap().latest().unwrap(),
NaiveDate::from_ymd_opt(1945, 2, 28).unwrap()
);
assert_eq!(
DicomDate::try_from(&NaiveDate::from_ymd_opt(1945, 2, 28).unwrap()).unwrap(),
DicomDate(DicomDateImpl::Day(1945, 2, 28))
);
assert!(matches!(
DicomDate::try_from(&NaiveDate::from_ymd_opt(-2000, 2, 28).unwrap()),
Err(Error::Conversion { .. })
));
assert!(matches!(
DicomDate::try_from(&NaiveDate::from_ymd_opt(10_000, 2, 28).unwrap()),
Err(Error::InvalidComponent {
component: DateComponent::Year,
..
})
));
}
#[test]
fn test_dicom_time() {
assert_eq!(
DicomTime::from_hms_micro(9, 1, 1, 123456).unwrap(),
DicomTime(DicomTimeImpl::Fraction(9, 1, 1, 123456, 6))
);
assert_eq!(
DicomTime::from_hms_micro(9, 1, 1, 1).unwrap(),
DicomTime(DicomTimeImpl::Fraction(9, 1, 1, 1, 6))
);
assert_eq!(
DicomTime::from_hms(9, 0, 0).unwrap(),
DicomTime(DicomTimeImpl::Second(9, 0, 0))
);
assert_eq!(
DicomTime::from_hm(23, 59).unwrap(),
DicomTime(DicomTimeImpl::Minute(23, 59))
);
assert_eq!(
DicomTime::from_h(1).unwrap(),
DicomTime(DicomTimeImpl::Hour(1))
);
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)
.unwrap()
.earliest()
.unwrap(),
NaiveTime::from_hms_micro_opt(9, 1, 1, 123_000).unwrap()
);
assert_eq!(
DicomTime::from_hms_milli(9, 1, 1, 123)
.unwrap()
.latest()
.unwrap(),
NaiveTime::from_hms_micro_opt(9, 1, 1, 123_999).unwrap()
);
assert_eq!(
DicomTime::from_hms_milli(9, 1, 1, 2)
.unwrap()
.earliest()
.unwrap(),
NaiveTime::from_hms_micro_opt(9, 1, 1, 002000).unwrap()
);
assert_eq!(
DicomTime::from_hms_milli(9, 1, 1, 2)
.unwrap()
.latest()
.unwrap(),
NaiveTime::from_hms_micro_opt(9, 1, 1, 002999).unwrap()
);
assert_eq!(
DicomTime::from_hms_micro(9, 1, 1, 123456)
.unwrap()
.is_precise(),
true
);
assert_eq!(
DicomTime::from_hms_milli(9, 1, 1, 1).unwrap(),
DicomTime(DicomTimeImpl::Fraction(9, 1, 1, 1, 3))
);
assert_eq!(
DicomTime::try_from(&NaiveTime::from_hms_milli_opt(16, 31, 28, 123).unwrap()).unwrap(),
DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 123_000, 6))
);
assert_eq!(
DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 28, 123).unwrap()).unwrap(),
DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 000123, 6))
);
assert_eq!(
DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 28, 1234).unwrap()).unwrap(),
DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 001234, 6))
);
assert_eq!(
DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 28, 0).unwrap()).unwrap(),
DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 0, 6))
);
assert_eq!(
DicomTime::from_hmsf(9, 1, 1, 1, 4).unwrap().to_string(),
"09:01:01.0001"
);
assert_eq!(
DicomTime::from_hmsf(9, 1, 1, 0, 1).unwrap().to_string(),
"09:01:01.0"
);
assert_eq!(
DicomTime::from_hmsf(7, 55, 1, 1, 5).unwrap().to_encoded(),
"075501.00001"
);
assert_eq!(
DicomTime::from_hmsf(9, 1, 1, 0, 2).unwrap().to_encoded(),
"090101.00"
);
assert_eq!(
DicomTime::from_hmsf(9, 1, 1, 0, 3).unwrap().to_encoded(),
"090101.000"
);
assert_eq!(
DicomTime::from_hmsf(9, 1, 1, 0, 4).unwrap().to_encoded(),
"090101.0000"
);
assert_eq!(
DicomTime::from_hmsf(9, 1, 1, 0, 5).unwrap().to_encoded(),
"090101.00000"
);
assert_eq!(
DicomTime::from_hmsf(9, 1, 1, 0, 6).unwrap().to_encoded(),
"090101.000000"
);
assert_eq!(
DicomTime::from_hmsf(23, 59, 60, 123, 3)
.unwrap()
.to_encoded(),
"235960.123",
);
assert_eq!(
DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 59, 1_000_000).unwrap())
.unwrap()
.to_encoded(),
"163160.000000",
);
assert_eq!(
DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 59, 1_012_345).unwrap())
.unwrap()
.to_encoded(),
"163160.012345",
);
assert_eq!(
DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 59, 0).unwrap())
.unwrap()
.to_encoded(),
"163159.000000",
);
let date_time: DateTime<_> = DateTime::<chrono::Utc>::from_naive_utc_and_offset(
NaiveDateTime::new(
NaiveDate::from_ymd_opt(2024, 8, 9).unwrap(),
NaiveTime::from_hms_opt(9, 9, 39).unwrap(),
),
chrono::Utc,
).with_timezone(&FixedOffset::east_opt(0).unwrap());
let dicom_date_time = DicomDateTime::try_from(&date_time).unwrap();
assert!(dicom_date_time.has_time_zone());
assert!(dicom_date_time.is_precise());
let dicom_time = dicom_date_time.time().unwrap();
assert_eq!(
dicom_time.fraction_and_precision(),
Some((&0, &6)),
);
assert_eq!(
dicom_date_time.to_encoded(),
"20240809090939.000000+0000"
);
assert!(matches!(
DicomTime::from_hmsf(9, 1, 1, 1, 7),
Err(Error::FractionPrecisionRange { value: 7, .. })
));
assert!(matches!(
DicomTime::from_hms_milli(9, 1, 1, 1000),
Err(Error::InvalidComponent {
component: DateComponent::Millisecond,
..
})
));
assert!(matches!(
DicomTime::from_hmsf(9, 1, 1, 123456, 3),
Err(Error::FractionPrecisionMismatch {
fraction: 123456,
precision: 3,
..
})
));
assert!(matches!(
DicomTime::from_hmsf(9, 1, 1, 1_000_000, 6),
Err(Error::InvalidComponent {
component: DateComponent::Fraction,
..
})
));
assert!(matches!(
DicomTime::from_hmsf(9, 1, 1, 12345, 5).unwrap().exact(),
Err(crate::value::range::Error::ImpreciseValue { .. })
));
}
#[test]
fn test_dicom_datetime() {
let default_offset = FixedOffset::east_opt(0).unwrap();
assert_eq!(
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,
time_zone: Some(default_offset)
}
);
assert_eq!(
DicomDateTime::from_date(DicomDate::from_ym(2020, 2).unwrap())
.earliest()
.unwrap(),
PreciseDateTime::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_with_time_zone(
DicomDate::from_ym(2020, 2).unwrap(),
default_offset
)
.latest()
.unwrap(),
PreciseDateTime::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()
)
);
assert_eq!(
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
)
.unwrap()
.earliest()
.unwrap(),
PreciseDateTime::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()
)
);
assert_eq!(
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
)
.unwrap()
.latest()
.unwrap(),
PreciseDateTime::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()
)
);
assert_eq!(
DicomDateTime::try_from(
&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()
)
.unwrap(),
DicomDateTime {
date: DicomDate::from_ymd(2020, 2, 29).unwrap(),
time: Some(DicomTime::from_hms_micro(23, 59, 59, 999_999).unwrap()),
time_zone: Some(default_offset)
}
);
assert_eq!(
DicomDateTime::try_from(
&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, 0).unwrap()
))
.unwrap()
)
.unwrap(),
DicomDateTime {
date: DicomDate::from_ymd(2020, 2, 29).unwrap(),
time: Some(DicomTime::from_hms_micro(23, 59, 59, 0).unwrap()),
time_zone: Some(default_offset)
}
);
assert_eq!(
DicomDateTime::try_from(
&FixedOffset::east_opt(0)
.unwrap()
.from_local_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(2023, 12, 31).unwrap(),
NaiveTime::from_hms_micro_opt(23, 59, 59, 1_000_000).unwrap()
))
.unwrap()
)
.unwrap(),
DicomDateTime {
date: DicomDate::from_ymd(2023, 12, 31).unwrap(),
time: Some(DicomTime::from_hms_micro(23, 59, 60, 0).unwrap()),
time_zone: Some(default_offset)
}
);
assert!(matches!(
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_with_time_zone(
DicomDate::from_ym(2020, 2).unwrap(),
DicomTime::from_hms_milli(23, 59, 59, 999).unwrap(),
default_offset
),
Err(Error::DateTimeFromPartials {
value: DateComponent::Month,
..
})
));
assert!(matches!(
DicomDateTime::from_date_and_time_with_time_zone(
DicomDate::from_y(1).unwrap(),
DicomTime::from_hms_micro(23, 59, 59, 10).unwrap(),
default_offset
),
Err(Error::DateTimeFromPartials {
value: DateComponent::Year,
..
})
));
assert!(matches!(
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
)
.unwrap()
.exact(),
Err(crate::value::range::Error::ImpreciseValue { .. })
));
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());
}
}