1use crate::value::AsRange;
4use chrono::{
5 DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc,
6};
7use snafu::{Backtrace, ResultExt, Snafu};
8use std::convert::{TryFrom, TryInto};
9use std::fmt;
10use std::ops::RangeInclusive;
11
12#[derive(Debug, Snafu)]
13#[non_exhaustive]
14pub enum Error {
15 #[snafu(display(
16 "To combine a DicomDate with a DicomTime value, the DicomDate has to be precise. Precision is: '{:?}'",
17 value
18 ))]
19 DateTimeFromPartials {
20 value: DateComponent,
21 backtrace: Backtrace,
22 },
23 #[snafu(display(
24 "'{:?}' has invalid value: '{}', must be in {:?}",
25 component,
26 value,
27 range
28 ))]
29 InvalidComponent {
30 component: DateComponent,
31 value: u32,
32 range: RangeInclusive<u32>,
33 backtrace: Backtrace,
34 },
35 #[snafu(display(
36 "Second fraction precision '{}' is out of range, must be in 0..=6",
37 value
38 ))]
39 FractionPrecisionRange { value: u32, backtrace: Backtrace },
40 #[snafu(display(
41 "Number of digits in decimal representation of fraction '{}' does not match it's precision '{}'",
42 fraction,
43 precision
44 ))]
45 FractionPrecisionMismatch {
46 fraction: u32,
47 precision: u32,
48 backtrace: Backtrace,
49 },
50 #[snafu(display("Conversion of value '{}' into {:?} failed", value, component))]
51 Conversion {
52 value: String,
53 component: DateComponent,
54 source: std::num::TryFromIntError,
55 },
56 #[snafu(display(
57 "Cannot convert from an imprecise value. This value represents a date / time range"
58 ))]
59 ImpreciseValue { backtrace: Backtrace },
60}
61
62type Result<T, E = Error> = std::result::Result<T, E>;
63
64#[derive(Debug, PartialEq, Copy, Clone, Eq, Hash, PartialOrd, Ord)]
66pub enum DateComponent {
67 Year,
69 Month,
71 Day,
73 Hour,
75 Minute,
77 Second,
79 Millisecond,
81 Fraction,
83 UtcWest,
85 UtcEast,
87}
88
89#[derive(Clone, Copy, PartialEq)]
121pub struct DicomDate(DicomDateImpl);
122
123#[derive(Clone, Copy, PartialEq)]
168pub struct DicomTime(DicomTimeImpl);
169
170#[derive(Debug, Clone, Copy, PartialEq)]
173enum DicomDateImpl {
174 Year(u16),
175 Month(u16, u8),
176 Day(u16, u8, u8),
177}
178
179#[derive(Debug, Clone, Copy, PartialEq)]
184enum DicomTimeImpl {
185 Hour(u8),
186 Minute(u8, u8),
187 Second(u8, u8, u8),
188 Fraction(u8, u8, u8, u32, u8),
189}
190
191#[derive(PartialEq, Clone, Copy)]
247pub struct DicomDateTime {
248 date: DicomDate,
249 time: Option<DicomTime>,
250 time_zone: Option<FixedOffset>,
251}
252
253pub fn check_component<T>(component: DateComponent, value: &T) -> Result<()>
257where
258 T: Into<u32> + Copy,
259{
260 let range = match component {
261 DateComponent::Year => 0..=9_999,
262 DateComponent::Month => 1..=12,
263 DateComponent::Day => 1..=31,
264 DateComponent::Hour => 0..=23,
265 DateComponent::Minute => 0..=59,
266 DateComponent::Second => 0..=60,
267 DateComponent::Millisecond => 0..=999,
268 DateComponent::Fraction => 0..=999_999,
269 DateComponent::UtcWest => 0..=(12 * 3600),
270 DateComponent::UtcEast => 0..=(14 * 3600),
271 };
272
273 let value: u32 = (*value).into();
274 if range.contains(&value) {
275 Ok(())
276 } else {
277 InvalidComponentSnafu {
278 component,
279 value,
280 range,
281 }
282 .fail()
283 }
284}
285
286impl DicomDate {
287 pub fn from_y(year: u16) -> Result<DicomDate> {
292 check_component(DateComponent::Year, &year)?;
293 Ok(DicomDate(DicomDateImpl::Year(year)))
294 }
295 pub fn from_ym(year: u16, month: u8) -> Result<DicomDate> {
300 check_component(DateComponent::Year, &year)?;
301 check_component(DateComponent::Month, &month)?;
302 Ok(DicomDate(DicomDateImpl::Month(year, month)))
303 }
304 pub fn from_ymd(year: u16, month: u8, day: u8) -> Result<DicomDate> {
309 check_component(DateComponent::Year, &year)?;
310 check_component(DateComponent::Month, &month)?;
311 check_component(DateComponent::Day, &day)?;
312 Ok(DicomDate(DicomDateImpl::Day(year, month, day)))
313 }
314
315 pub fn year(&self) -> &u16 {
317 match self {
318 DicomDate(DicomDateImpl::Year(y)) => y,
319 DicomDate(DicomDateImpl::Month(y, _)) => y,
320 DicomDate(DicomDateImpl::Day(y, _, _)) => y,
321 }
322 }
323 pub fn month(&self) -> Option<&u8> {
325 match self {
326 DicomDate(DicomDateImpl::Year(_)) => None,
327 DicomDate(DicomDateImpl::Month(_, m)) => Some(m),
328 DicomDate(DicomDateImpl::Day(_, m, _)) => Some(m),
329 }
330 }
331 pub fn day(&self) -> Option<&u8> {
333 match self {
334 DicomDate(DicomDateImpl::Year(_)) => None,
335 DicomDate(DicomDateImpl::Month(_, _)) => None,
336 DicomDate(DicomDateImpl::Day(_, _, d)) => Some(d),
337 }
338 }
339
340 pub(crate) fn precision(&self) -> DateComponent {
342 match self {
343 DicomDate(DicomDateImpl::Year(..)) => DateComponent::Year,
344 DicomDate(DicomDateImpl::Month(..)) => DateComponent::Month,
345 DicomDate(DicomDateImpl::Day(..)) => DateComponent::Day,
346 }
347 }
348}
349
350impl std::str::FromStr for DicomDate {
351 type Err = crate::value::DeserializeError;
352
353 fn from_str(s: &str) -> Result<Self, Self::Err> {
354 let (date, _) = crate::value::deserialize::parse_date_partial(s.as_bytes())?;
355 Ok(date)
356 }
357}
358
359impl TryFrom<&NaiveDate> for DicomDate {
360 type Error = Error;
361 fn try_from(date: &NaiveDate) -> Result<Self> {
362 let year: u16 = date.year().try_into().with_context(|_| ConversionSnafu {
363 value: date.year().to_string(),
364 component: DateComponent::Year,
365 })?;
366 let month: u8 = date.month().try_into().with_context(|_| ConversionSnafu {
367 value: date.month().to_string(),
368 component: DateComponent::Month,
369 })?;
370 let day: u8 = date.day().try_into().with_context(|_| ConversionSnafu {
371 value: date.day().to_string(),
372 component: DateComponent::Day,
373 })?;
374 DicomDate::from_ymd(year, month, day)
375 }
376}
377
378impl fmt::Display for DicomDate {
379 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380 match self {
381 DicomDate(DicomDateImpl::Year(y)) => write!(f, "{y:04}"),
382 DicomDate(DicomDateImpl::Month(y, m)) => write!(f, "{y:04}-{m:02}"),
383 DicomDate(DicomDateImpl::Day(y, m, d)) => write!(f, "{y:04}-{m:02}-{d:02}"),
384 }
385 }
386}
387
388impl fmt::Debug for DicomDate {
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 match self {
391 DicomDate(DicomDateImpl::Year(y)) => write!(f, "{y:04}-MM-DD"),
392 DicomDate(DicomDateImpl::Month(y, m)) => write!(f, "{y:04}-{m:02}-DD"),
393 DicomDate(DicomDateImpl::Day(y, m, d)) => write!(f, "{y:04}-{m:02}-{d:02}"),
394 }
395 }
396}
397
398impl DicomTime {
399 pub fn from_h(hour: u8) -> Result<DicomTime> {
404 check_component(DateComponent::Hour, &hour)?;
405 Ok(DicomTime(DicomTimeImpl::Hour(hour)))
406 }
407
408 pub fn from_hm(hour: u8, minute: u8) -> Result<DicomTime> {
413 check_component(DateComponent::Hour, &hour)?;
414 check_component(DateComponent::Minute, &minute)?;
415 Ok(DicomTime(DicomTimeImpl::Minute(hour, minute)))
416 }
417
418 pub fn from_hms(hour: u8, minute: u8, second: u8) -> Result<DicomTime> {
423 check_component(DateComponent::Hour, &hour)?;
424 check_component(DateComponent::Minute, &minute)?;
425 check_component(DateComponent::Second, &second)?;
426 Ok(DicomTime(DicomTimeImpl::Second(hour, minute, second)))
427 }
428 pub fn from_hms_milli(hour: u8, minute: u8, second: u8, millisecond: u32) -> Result<DicomTime> {
433 check_component(DateComponent::Millisecond, &millisecond)?;
434 Ok(DicomTime(DicomTimeImpl::Fraction(
435 hour,
436 minute,
437 second,
438 millisecond,
439 3,
440 )))
441 }
442
443 pub fn from_hms_micro(hour: u8, minute: u8, second: u8, microsecond: u32) -> Result<DicomTime> {
449 check_component(DateComponent::Fraction, µsecond)?;
450 Ok(DicomTime(DicomTimeImpl::Fraction(
451 hour,
452 minute,
453 second,
454 microsecond,
455 6,
456 )))
457 }
458
459 pub fn hour(&self) -> &u8 {
461 match self {
462 DicomTime(DicomTimeImpl::Hour(h)) => h,
463 DicomTime(DicomTimeImpl::Minute(h, _)) => h,
464 DicomTime(DicomTimeImpl::Second(h, _, _)) => h,
465 DicomTime(DicomTimeImpl::Fraction(h, _, _, _, _)) => h,
466 }
467 }
468 pub fn minute(&self) -> Option<&u8> {
470 match self {
471 DicomTime(DicomTimeImpl::Hour(_)) => None,
472 DicomTime(DicomTimeImpl::Minute(_, m)) => Some(m),
473 DicomTime(DicomTimeImpl::Second(_, m, _)) => Some(m),
474 DicomTime(DicomTimeImpl::Fraction(_, m, _, _, _)) => Some(m),
475 }
476 }
477 pub fn second(&self) -> Option<&u8> {
479 match self {
480 DicomTime(DicomTimeImpl::Hour(_)) => None,
481 DicomTime(DicomTimeImpl::Minute(_, _)) => None,
482 DicomTime(DicomTimeImpl::Second(_, _, s)) => Some(s),
483 DicomTime(DicomTimeImpl::Fraction(_, _, s, _, _)) => Some(s),
484 }
485 }
486
487 pub fn millisecond(&self) -> Option<u32> {
492 self.fraction_and_precision().and_then(|(f, fp)| match fp {
493 0..=2 => None,
494 3 => Some(f),
495 4 => Some(f / 10),
496 5 => Some(f / 100),
497 6 => Some(f / 1_000),
498 _ => unreachable!("fp outside expected range 0..=6"),
499 })
500 }
501
502 pub fn fraction_micro(&self) -> Option<u32> {
518 match self.fraction_and_precision() {
519 None => None,
520 Some((f, 1)) => Some(f * 100_000),
521 Some((f, 2)) => Some(f * 10_000),
522 Some((f, 3)) => Some(f * 1_000),
523 Some((f, 4)) => Some(f * 100),
524 Some((f, 5)) => Some(f * 10),
525 Some((f, 6)) => Some(f),
526 Some((_, _)) => unreachable!("fp outside expected range 0..=6"),
527 }
528 }
529
530 pub fn fraction_ms(&self) -> Option<u32> {
549 match self.fraction_and_precision() {
550 None => None,
551 Some((f, 1)) => Some(f * 100),
552 Some((f, 2)) => Some(f * 10),
553 Some((f, 3)) => Some(f),
554 Some((f, 4)) => Some(f / 10),
555 Some((f, 5)) => Some(f / 100),
556 Some((f, 6)) => Some(f / 1_000),
557 Some((_, _)) => unreachable!("fp outside expected range 0..=6"),
558 }
559 }
560
561 pub fn fraction_precision(&self) -> u8 {
564 match self.fraction_and_precision() {
565 None => 0,
566 Some((_, fp)) => fp,
567 }
568 }
569
570 pub(crate) fn fraction_and_precision(&self) -> Option<(u32, u8)> {
575 match self {
576 DicomTime(DicomTimeImpl::Hour(_)) => None,
577 DicomTime(DicomTimeImpl::Minute(_, _)) => None,
578 DicomTime(DicomTimeImpl::Second(_, _, _)) => None,
579 DicomTime(DicomTimeImpl::Fraction(_, _, _, f, fp)) => Some((*f, *fp)),
580 }
581 }
582
583 pub fn fraction_str(&self) -> String {
600 match self.fraction_and_precision() {
601 None | Some((_, 0)) => String::new(),
602 Some((f, 1)) => format!("{:01}", f),
603 Some((f, 2)) => format!("{:02}", f),
604 Some((f, 3)) => format!("{:03}", f),
605 Some((f, 4)) => format!("{:04}", f),
606 Some((f, 5)) => format!("{:05}", f),
607 Some((f, 6)) => format!("{:06}", f),
608 Some((_, _)) => unreachable!("fp outside expected range 0..=6"),
609 }
610 }
611
612 pub(crate) fn from_hmsf(
617 hour: u8,
618 minute: u8,
619 second: u8,
620 fraction: u32,
621 frac_precision: u8,
622 ) -> Result<DicomTime> {
623 if !(1..=6).contains(&frac_precision) {
624 return FractionPrecisionRangeSnafu {
625 value: frac_precision,
626 }
627 .fail();
628 }
629 if u32::pow(10, frac_precision as u32) < fraction {
630 return FractionPrecisionMismatchSnafu {
631 fraction,
632 precision: frac_precision,
633 }
634 .fail();
635 }
636
637 check_component(DateComponent::Hour, &hour)?;
638 check_component(DateComponent::Minute, &minute)?;
639 check_component(DateComponent::Second, &second)?;
640 let f: u32 = fraction * u32::pow(10, 6 - frac_precision as u32);
641 check_component(DateComponent::Fraction, &f)?;
642 Ok(DicomTime(DicomTimeImpl::Fraction(
643 hour,
644 minute,
645 second,
646 fraction,
647 frac_precision,
648 )))
649 }
650
651 pub(crate) fn precision(&self) -> DateComponent {
653 match self {
654 DicomTime(DicomTimeImpl::Hour(..)) => DateComponent::Hour,
655 DicomTime(DicomTimeImpl::Minute(..)) => DateComponent::Minute,
656 DicomTime(DicomTimeImpl::Second(..)) => DateComponent::Second,
657 DicomTime(DicomTimeImpl::Fraction(..)) => DateComponent::Fraction,
658 }
659 }
660}
661
662impl TryFrom<&NaiveTime> for DicomTime {
663 type Error = Error;
664 fn try_from(time: &NaiveTime) -> Result<Self> {
665 let hour: u8 = time.hour().try_into().with_context(|_| ConversionSnafu {
666 value: time.hour().to_string(),
667 component: DateComponent::Hour,
668 })?;
669 let minute: u8 = time.minute().try_into().with_context(|_| ConversionSnafu {
670 value: time.minute().to_string(),
671 component: DateComponent::Minute,
672 })?;
673 let second: u8 = time.second().try_into().with_context(|_| ConversionSnafu {
674 value: time.second().to_string(),
675 component: DateComponent::Second,
676 })?;
677 let microsecond = time.nanosecond() / 1000;
678 let (second, microsecond) = if microsecond >= 1_000_000 && second == 59 {
680 (60, microsecond - 1_000_000)
681 } else {
682 (second, microsecond)
683 };
684
685 DicomTime::from_hms_micro(hour, minute, second, microsecond)
686 }
687}
688
689impl std::str::FromStr for DicomTime {
690 type Err = crate::value::DeserializeError;
691
692 fn from_str(s: &str) -> Result<Self, Self::Err> {
693 let (time, _) = crate::value::deserialize::parse_time_partial(s.as_bytes())?;
694 Ok(time)
695 }
696}
697
698impl fmt::Display for DicomTime {
699 fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result {
700 match self {
701 DicomTime(DicomTimeImpl::Hour(h)) => write!(frm, "{h:02}"),
702 DicomTime(DicomTimeImpl::Minute(h, m)) => write!(frm, "{h:02}:{m:02}"),
703 DicomTime(DicomTimeImpl::Second(h, m, s)) => {
704 write!(frm, "{h:02}:{m:02}:{s:02}")
705 }
706 DicomTime(DicomTimeImpl::Fraction(h, m, s, f, fp)) => {
707 let sfrac = (u32::pow(10, *fp as u32) + f).to_string();
708 write!(
709 frm,
710 "{h:02}:{m:02}:{s:02}.{}",
711 match f {
712 0 => "0",
713 _ => sfrac.get(1..).unwrap(),
714 }
715 )
716 }
717 }
718 }
719}
720
721impl fmt::Debug for DicomTime {
722 fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result {
723 match self {
724 DicomTime(DicomTimeImpl::Hour(h)) => write!(frm, "{h:02}:mm:ss.FFFFFF"),
725 DicomTime(DicomTimeImpl::Minute(h, m)) => write!(frm, "{h:02}:{m:02}:ss.FFFFFF"),
726 DicomTime(DicomTimeImpl::Second(h, m, s)) => {
727 write!(frm, "{h:02}:{m:02}:{s:02}.FFFFFF")
728 }
729 DicomTime(DicomTimeImpl::Fraction(h, m, s, f, _fp)) => {
730 write!(frm, "{h:02}:{m:02}:{s:02}.{f:F<6}")
731 }
732 }
733 }
734}
735
736impl DicomDateTime {
737 pub fn from_date_with_time_zone(date: DicomDate, time_zone: FixedOffset) -> DicomDateTime {
741 DicomDateTime {
742 date,
743 time: None,
744 time_zone: Some(time_zone),
745 }
746 }
747
748 pub fn from_date(date: DicomDate) -> DicomDateTime {
752 DicomDateTime {
753 date,
754 time: None,
755 time_zone: None,
756 }
757 }
758
759 pub fn from_date_and_time(date: DicomDate, time: DicomTime) -> Result<DicomDateTime> {
764 if date.is_precise() {
765 Ok(DicomDateTime {
766 date,
767 time: Some(time),
768 time_zone: None,
769 })
770 } else {
771 DateTimeFromPartialsSnafu {
772 value: date.precision(),
773 }
774 .fail()
775 }
776 }
777
778 pub fn from_date_and_time_with_time_zone(
783 date: DicomDate,
784 time: DicomTime,
785 time_zone: FixedOffset,
786 ) -> Result<DicomDateTime> {
787 if date.is_precise() {
788 Ok(DicomDateTime {
789 date,
790 time: Some(time),
791 time_zone: Some(time_zone),
792 })
793 } else {
794 DateTimeFromPartialsSnafu {
795 value: date.precision(),
796 }
797 .fail()
798 }
799 }
800
801 pub fn now_local() -> Result<DicomDateTime> {
806 DicomDateTime::try_from(&Local::now().naive_local())
807 }
808
809 pub fn now_utc() -> Result<DicomDateTime> {
814 DicomDateTime::try_from(&Utc::now().naive_utc())
815 }
816
817 pub fn into_parts(self) -> (DicomDate, Option<DicomTime>, Option<FixedOffset>) {
821 (self.date, self.time, self.time_zone)
822 }
823
824 pub fn date(&self) -> &DicomDate {
826 &self.date
827 }
828
829 pub fn time(&self) -> Option<&DicomTime> {
831 self.time.as_ref()
832 }
833
834 pub fn time_zone(&self) -> Option<&FixedOffset> {
836 self.time_zone.as_ref()
837 }
838
839 pub fn has_time_zone(&self) -> bool {
841 self.time_zone.is_some()
842 }
843}
844
845impl TryFrom<&DateTime<FixedOffset>> for DicomDateTime {
846 type Error = Error;
847 fn try_from(dt: &DateTime<FixedOffset>) -> Result<Self> {
848 let year: u16 = dt.year().try_into().with_context(|_| ConversionSnafu {
849 value: dt.year().to_string(),
850 component: DateComponent::Year,
851 })?;
852 let month: u8 = dt.month().try_into().with_context(|_| ConversionSnafu {
853 value: dt.month().to_string(),
854 component: DateComponent::Month,
855 })?;
856 let day: u8 = dt.day().try_into().with_context(|_| ConversionSnafu {
857 value: dt.day().to_string(),
858 component: DateComponent::Day,
859 })?;
860 let hour: u8 = dt.hour().try_into().with_context(|_| ConversionSnafu {
861 value: dt.hour().to_string(),
862 component: DateComponent::Hour,
863 })?;
864 let minute: u8 = dt.minute().try_into().with_context(|_| ConversionSnafu {
865 value: dt.minute().to_string(),
866 component: DateComponent::Minute,
867 })?;
868 let second: u8 = dt.second().try_into().with_context(|_| ConversionSnafu {
869 value: dt.second().to_string(),
870 component: DateComponent::Second,
871 })?;
872 let microsecond = dt.nanosecond() / 1000;
873 let (second, microsecond) = if microsecond >= 1_000_000 && second == 59 {
875 (60, microsecond - 1_000_000)
876 } else {
877 (second, microsecond)
878 };
879
880 DicomDateTime::from_date_and_time_with_time_zone(
881 DicomDate::from_ymd(year, month, day)?,
882 DicomTime::from_hms_micro(hour, minute, second, microsecond)?,
883 *dt.offset(),
884 )
885 }
886}
887
888impl TryFrom<&NaiveDateTime> for DicomDateTime {
889 type Error = Error;
890 fn try_from(dt: &NaiveDateTime) -> Result<Self> {
891 let year: u16 = dt.year().try_into().with_context(|_| ConversionSnafu {
892 value: dt.year().to_string(),
893 component: DateComponent::Year,
894 })?;
895 let month: u8 = dt.month().try_into().with_context(|_| ConversionSnafu {
896 value: dt.month().to_string(),
897 component: DateComponent::Month,
898 })?;
899 let day: u8 = dt.day().try_into().with_context(|_| ConversionSnafu {
900 value: dt.day().to_string(),
901 component: DateComponent::Day,
902 })?;
903 let hour: u8 = dt.hour().try_into().with_context(|_| ConversionSnafu {
904 value: dt.hour().to_string(),
905 component: DateComponent::Hour,
906 })?;
907 let minute: u8 = dt.minute().try_into().with_context(|_| ConversionSnafu {
908 value: dt.minute().to_string(),
909 component: DateComponent::Minute,
910 })?;
911 let second: u8 = dt.second().try_into().with_context(|_| ConversionSnafu {
912 value: dt.second().to_string(),
913 component: DateComponent::Second,
914 })?;
915 let microsecond = dt.nanosecond() / 1000;
916 let (second, microsecond) = if microsecond >= 1_000_000 && second == 59 {
918 (60, microsecond - 1_000_000)
919 } else {
920 (second, microsecond)
921 };
922
923 DicomDateTime::from_date_and_time(
924 DicomDate::from_ymd(year, month, day)?,
925 DicomTime::from_hms_micro(hour, minute, second, microsecond)?,
926 )
927 }
928}
929
930impl fmt::Display for DicomDateTime {
931 fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result {
932 match self.time {
933 None => match self.time_zone {
934 Some(offset) => write!(frm, "{} {}", self.date, offset),
935 None => write!(frm, "{}", self.date),
936 },
937 Some(time) => match self.time_zone {
938 Some(offset) => write!(frm, "{} {} {}", self.date, time, offset),
939 None => write!(frm, "{} {}", self.date, time),
940 },
941 }
942 }
943}
944
945impl fmt::Debug for DicomDateTime {
946 fn fmt(&self, frm: &mut fmt::Formatter<'_>) -> fmt::Result {
947 match self.time {
948 None => match self.time_zone {
949 Some(offset) => write!(frm, "{:?} {}", self.date, offset),
950 None => write!(frm, "{:?}", self.date),
951 },
952 Some(time) => match self.time_zone {
953 Some(offset) => write!(frm, "{:?} {:?} {}", self.date, time, offset),
954 None => write!(frm, "{:?} {:?}", self.date, time),
955 },
956 }
957 }
958}
959
960impl std::str::FromStr for DicomDateTime {
961 type Err = crate::value::DeserializeError;
962
963 fn from_str(s: &str) -> Result<Self, Self::Err> {
964 crate::value::deserialize::parse_datetime_partial(s.as_bytes())
965 }
966}
967
968impl DicomDate {
969 pub fn to_encoded(&self) -> String {
973 match self {
974 DicomDate(DicomDateImpl::Year(y)) => format!("{y:04}"),
975 DicomDate(DicomDateImpl::Month(y, m)) => format!("{y:04}{m:02}"),
976 DicomDate(DicomDateImpl::Day(y, m, d)) => format!("{y:04}{m:02}{d:02}"),
977 }
978 }
979}
980
981impl DicomTime {
982 pub fn to_encoded(&self) -> String {
986 match self {
987 DicomTime(DicomTimeImpl::Hour(h)) => format!("{h:02}"),
988 DicomTime(DicomTimeImpl::Minute(h, m)) => format!("{h:02}{m:02}"),
989 DicomTime(DicomTimeImpl::Second(h, m, s)) => format!("{h:02}{m:02}{s:02}"),
990 DicomTime(DicomTimeImpl::Fraction(h, m, s, f, fp)) => {
991 let sfrac = (u32::pow(10, *fp as u32) + f).to_string();
992 format!("{h:02}{m:02}{s:02}.{}", sfrac.get(1..).unwrap())
993 }
994 }
995 }
996}
997
998impl DicomDateTime {
999 pub fn to_encoded(&self) -> String {
1003 match self.time {
1004 Some(time) => match self.time_zone {
1005 Some(offset) => format!(
1006 "{}{}{}",
1007 self.date.to_encoded(),
1008 time.to_encoded(),
1009 offset.to_string().replace(':', "")
1010 ),
1011 None => format!("{}{}", self.date.to_encoded(), time.to_encoded()),
1012 },
1013 None => match self.time_zone {
1014 Some(offset) => format!(
1015 "{}{}",
1016 self.date.to_encoded(),
1017 offset.to_string().replace(':', "")
1018 ),
1019 None => self.date.to_encoded().to_string(),
1020 },
1021 }
1022 }
1023}
1024
1025#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
1032pub enum PreciseDateTime {
1033 Naive(NaiveDateTime),
1035 TimeZone(DateTime<FixedOffset>),
1037}
1038
1039impl PreciseDateTime {
1040 pub fn as_datetime(&self) -> Option<&DateTime<FixedOffset>> {
1043 match self {
1044 PreciseDateTime::Naive(..) => None,
1045 PreciseDateTime::TimeZone(value) => Some(value),
1046 }
1047 }
1048
1049 pub fn as_naive_datetime(&self) -> Option<&NaiveDateTime> {
1052 match self {
1053 PreciseDateTime::Naive(value) => Some(value),
1054 PreciseDateTime::TimeZone(..) => None,
1055 }
1056 }
1057
1058 pub fn into_datetime(self) -> Option<DateTime<FixedOffset>> {
1061 match self {
1062 PreciseDateTime::Naive(..) => None,
1063 PreciseDateTime::TimeZone(value) => Some(value),
1064 }
1065 }
1066
1067 pub fn into_naive_datetime(self) -> Option<NaiveDateTime> {
1070 match self {
1071 PreciseDateTime::Naive(value) => Some(value),
1072 PreciseDateTime::TimeZone(..) => None,
1073 }
1074 }
1075
1076 pub fn to_naive_date(&self) -> NaiveDate {
1086 match self {
1087 PreciseDateTime::Naive(value) => value.date(),
1088 PreciseDateTime::TimeZone(value) => value.date_naive(),
1089 }
1090 }
1091
1092 pub fn to_naive_time(&self) -> NaiveTime {
1094 match self {
1095 PreciseDateTime::Naive(value) => value.time(),
1096 PreciseDateTime::TimeZone(value) => value.time(),
1097 }
1098 }
1099
1100 #[inline]
1102 pub fn has_time_zone(&self) -> bool {
1103 matches!(self, PreciseDateTime::TimeZone(..))
1104 }
1105}
1106
1107impl PartialOrd for PreciseDateTime {
1114 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1115 match (self, other) {
1116 (PreciseDateTime::Naive(a), PreciseDateTime::Naive(b)) => a.partial_cmp(b),
1117 (PreciseDateTime::TimeZone(a), PreciseDateTime::TimeZone(b)) => a.partial_cmp(b),
1118 _ => None,
1119 }
1120 }
1121}
1122
1123#[cfg(test)]
1124mod tests {
1125 use super::*;
1126 use chrono::{Duration, TimeZone};
1127
1128 #[test]
1129 fn test_dicom_date() {
1130 assert_eq!(
1131 DicomDate::from_ymd(1944, 2, 29).unwrap(),
1132 DicomDate(DicomDateImpl::Day(1944, 2, 29))
1133 );
1134
1135 assert!(DicomDate::from_ymd(1945, 2, 29).unwrap().is_precise());
1137 assert_eq!(
1138 DicomDate::from_ym(1944, 2).unwrap(),
1139 DicomDate(DicomDateImpl::Month(1944, 2))
1140 );
1141 assert_eq!(
1142 DicomDate::from_y(1944).unwrap(),
1143 DicomDate(DicomDateImpl::Year(1944))
1144 );
1145
1146 assert!(DicomDate::from_ymd(1944, 2, 29).unwrap().is_precise());
1147 assert!(!DicomDate::from_ym(1944, 2).unwrap().is_precise());
1148 assert!(!DicomDate::from_y(1944).unwrap().is_precise());
1149 assert_eq!(
1150 DicomDate::from_ymd(1944, 2, 29)
1151 .unwrap()
1152 .earliest()
1153 .unwrap(),
1154 NaiveDate::from_ymd_opt(1944, 2, 29).unwrap()
1155 );
1156 assert_eq!(
1157 DicomDate::from_ymd(1944, 2, 29).unwrap().latest().unwrap(),
1158 NaiveDate::from_ymd_opt(1944, 2, 29).unwrap()
1159 );
1160
1161 assert_eq!(
1162 DicomDate::from_y(1944).unwrap().earliest().unwrap(),
1163 NaiveDate::from_ymd_opt(1944, 1, 1).unwrap()
1164 );
1165 assert_eq!(
1167 DicomDate::from_ym(1944, 2).unwrap().latest().unwrap(),
1168 NaiveDate::from_ymd_opt(1944, 2, 29).unwrap()
1169 );
1170 assert_eq!(
1171 DicomDate::from_ym(1945, 2).unwrap().latest().unwrap(),
1172 NaiveDate::from_ymd_opt(1945, 2, 28).unwrap()
1173 );
1174
1175 assert_eq!(
1176 DicomDate::try_from(&NaiveDate::from_ymd_opt(1945, 2, 28).unwrap()).unwrap(),
1177 DicomDate(DicomDateImpl::Day(1945, 2, 28))
1178 );
1179
1180 let date: DicomDate = "20240229".parse().unwrap();
1183 assert_eq!(date, DicomDate(DicomDateImpl::Day(2024, 2, 29)));
1184 assert!(date.is_precise());
1185
1186 assert!(matches!(
1189 DicomDate::try_from(&NaiveDate::from_ymd_opt(-2000, 2, 28).unwrap()),
1190 Err(Error::Conversion { .. })
1191 ));
1192
1193 assert!(matches!(
1194 DicomDate::try_from(&NaiveDate::from_ymd_opt(10_000, 2, 28).unwrap()),
1195 Err(Error::InvalidComponent {
1196 component: DateComponent::Year,
1197 ..
1198 })
1199 ));
1200 }
1201
1202 #[test]
1203 fn test_dicom_time() {
1204 assert_eq!(
1205 DicomTime::from_hms_micro(9, 1, 1, 123456).unwrap(),
1206 DicomTime(DicomTimeImpl::Fraction(9, 1, 1, 123456, 6))
1207 );
1208 assert_eq!(
1209 DicomTime::from_hms_micro(9, 1, 1, 1).unwrap(),
1210 DicomTime(DicomTimeImpl::Fraction(9, 1, 1, 1, 6))
1211 );
1212 assert_eq!(
1213 DicomTime::from_hms(9, 0, 0).unwrap(),
1214 DicomTime(DicomTimeImpl::Second(9, 0, 0))
1215 );
1216 assert_eq!(
1217 DicomTime::from_hm(23, 59).unwrap(),
1218 DicomTime(DicomTimeImpl::Minute(23, 59))
1219 );
1220 assert_eq!(
1221 DicomTime::from_h(1).unwrap(),
1222 DicomTime(DicomTimeImpl::Hour(1))
1223 );
1224 assert!(
1226 DicomTime::from_hms_micro(9, 1, 1, 123456)
1227 .unwrap()
1228 .is_precise()
1229 );
1230 assert!(
1231 !DicomTime::from_hms_milli(9, 1, 1, 123)
1232 .unwrap()
1233 .is_precise()
1234 );
1235
1236 assert_eq!(
1237 DicomTime::from_hms_milli(9, 1, 1, 123)
1238 .unwrap()
1239 .earliest()
1240 .unwrap(),
1241 NaiveTime::from_hms_micro_opt(9, 1, 1, 123_000).unwrap()
1242 );
1243 assert_eq!(
1244 DicomTime::from_hms_milli(9, 1, 1, 123)
1245 .unwrap()
1246 .latest()
1247 .unwrap(),
1248 NaiveTime::from_hms_micro_opt(9, 1, 1, 123_999).unwrap()
1249 );
1250
1251 assert_eq!(
1252 DicomTime::from_hms_milli(9, 1, 1, 2)
1253 .unwrap()
1254 .earliest()
1255 .unwrap(),
1256 NaiveTime::from_hms_micro_opt(9, 1, 1, 2000).unwrap()
1257 );
1258 assert_eq!(
1259 DicomTime::from_hms_milli(9, 1, 1, 2)
1260 .unwrap()
1261 .latest()
1262 .unwrap(),
1263 NaiveTime::from_hms_micro_opt(9, 1, 1, 2999).unwrap()
1264 );
1265
1266 assert!(
1267 DicomTime::from_hms_micro(9, 1, 1, 123456)
1268 .unwrap()
1269 .is_precise()
1270 );
1271
1272 assert_eq!(
1273 DicomTime::from_hms_milli(9, 1, 1, 1).unwrap(),
1274 DicomTime(DicomTimeImpl::Fraction(9, 1, 1, 1, 3))
1275 );
1276
1277 assert_eq!(
1278 DicomTime::try_from(&NaiveTime::from_hms_milli_opt(16, 31, 28, 123).unwrap()).unwrap(),
1279 DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 123_000, 6))
1280 );
1281
1282 assert_eq!(
1283 DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 28, 123).unwrap()).unwrap(),
1284 DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 123, 6))
1285 );
1286
1287 assert_eq!(
1288 DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 28, 1234).unwrap()).unwrap(),
1289 DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 1234, 6))
1290 );
1291
1292 assert_eq!(
1293 DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 28, 0).unwrap()).unwrap(),
1294 DicomTime(DicomTimeImpl::Fraction(16, 31, 28, 0, 6))
1295 );
1296
1297 assert_eq!(
1298 DicomTime::from_hmsf(9, 1, 1, 1, 4).unwrap().to_string(),
1299 "09:01:01.0001"
1300 );
1301 assert_eq!(
1302 DicomTime::from_hmsf(9, 1, 1, 0, 1).unwrap().to_string(),
1303 "09:01:01.0"
1304 );
1305 assert_eq!(
1306 DicomTime::from_hmsf(7, 55, 1, 1, 5).unwrap().to_encoded(),
1307 "075501.00001"
1308 );
1309 assert_eq!(
1311 DicomTime::from_hmsf(9, 1, 1, 0, 2).unwrap().to_encoded(),
1312 "090101.00"
1313 );
1314 assert_eq!(
1315 DicomTime::from_hmsf(9, 1, 1, 0, 3).unwrap().to_encoded(),
1316 "090101.000"
1317 );
1318 assert_eq!(
1319 DicomTime::from_hmsf(9, 1, 1, 0, 4).unwrap().to_encoded(),
1320 "090101.0000"
1321 );
1322 assert_eq!(
1323 DicomTime::from_hmsf(9, 1, 1, 0, 5).unwrap().to_encoded(),
1324 "090101.00000"
1325 );
1326 assert_eq!(
1327 DicomTime::from_hmsf(9, 1, 1, 0, 6).unwrap().to_encoded(),
1328 "090101.000000"
1329 );
1330
1331 assert_eq!(
1333 DicomTime::from_hmsf(23, 59, 60, 123, 3)
1334 .unwrap()
1335 .to_encoded(),
1336 "235960.123",
1337 );
1338
1339 assert_eq!(
1341 DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 59, 1_000_000).unwrap())
1342 .unwrap()
1343 .to_encoded(),
1344 "163160.000000",
1345 );
1346
1347 {
1349 let time =
1350 DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 59, 1_012_345).unwrap())
1351 .unwrap();
1352 assert_eq!(time.to_encoded(), "163160.012345");
1353
1354 assert_eq!(time.fraction_micro(), Some(12_345));
1355 assert_eq!(time.fraction_ms(), Some(12));
1356 assert_eq!(time.millisecond(), Some(12));
1357 }
1358
1359 assert_eq!(
1361 DicomTime::try_from(&NaiveTime::from_hms_micro_opt(16, 31, 59, 0).unwrap())
1362 .unwrap()
1363 .to_encoded(),
1364 "163159.000000",
1365 );
1366
1367 let date_time: DateTime<_> = DateTime::<Utc>::from_naive_utc_and_offset(
1369 NaiveDateTime::new(
1370 NaiveDate::from_ymd_opt(2024, 8, 9).unwrap(),
1371 NaiveTime::from_hms_opt(9, 9, 39).unwrap(),
1372 ),
1373 Utc,
1374 )
1375 .with_timezone(&FixedOffset::east_opt(0).unwrap());
1376 let dicom_date_time = DicomDateTime::try_from(&date_time).unwrap();
1377 assert!(dicom_date_time.has_time_zone());
1378 assert!(dicom_date_time.is_precise());
1379 let dicom_time = dicom_date_time.time().unwrap();
1380 assert_eq!(dicom_time.fraction_and_precision(), Some((0, 6)),);
1381 assert_eq!(dicom_date_time.to_encoded(), "20240809090939.000000+0000");
1382
1383 assert_eq!(
1384 dicom_date_time.time().map(|t| t.millisecond()),
1385 Some(Some(0)),
1386 );
1387
1388 let time: DicomTime = "211133.7651".parse().unwrap();
1391 assert_eq!(
1392 time,
1393 DicomTime(DicomTimeImpl::Fraction(21, 11, 33, 7651, 4))
1394 );
1395 assert_eq!(time.fraction_ms(), Some(765));
1396 assert_eq!(time.fraction_micro(), Some(765_100));
1397 assert_eq!(time.fraction_precision(), 4);
1398 assert_eq!(&time.fraction_str(), "7651");
1399
1400 assert!(matches!(
1403 DicomTime::from_hmsf(9, 1, 1, 1, 7),
1404 Err(Error::FractionPrecisionRange { value: 7, .. })
1405 ));
1406
1407 assert!(matches!(
1408 DicomTime::from_hms_milli(9, 1, 1, 1000),
1409 Err(Error::InvalidComponent {
1410 component: DateComponent::Millisecond,
1411 ..
1412 })
1413 ));
1414
1415 assert!(matches!(
1416 DicomTime::from_hmsf(9, 1, 1, 123456, 3),
1417 Err(Error::FractionPrecisionMismatch {
1418 fraction: 123456,
1419 precision: 3,
1420 ..
1421 })
1422 ));
1423
1424 assert!(matches!(
1426 DicomTime::from_hmsf(9, 1, 1, 1_000_000, 6),
1427 Err(Error::InvalidComponent {
1428 component: DateComponent::Fraction,
1429 ..
1430 })
1431 ));
1432
1433 assert!(matches!(
1434 DicomTime::from_hmsf(9, 1, 1, 12345, 5).unwrap().exact(),
1435 Err(crate::value::range::Error::ImpreciseValue { .. })
1436 ));
1437
1438 for (frac, frac_precision, microseconds) in [
1440 (1, 1, 100_000),
1441 (12, 2, 120_000),
1442 (123, 3, 123_000),
1443 (1234, 4, 123_400),
1444 (12345, 5, 123_450),
1445 (123456, 6, 123_456),
1446 ] {
1447 let time = DicomTime::from_hmsf(9, 1, 1, frac, frac_precision).unwrap();
1448 assert_eq!(time.fraction_micro(), Some(microseconds));
1449 assert_eq!(time.fraction_precision(), frac_precision);
1450 assert_eq!(time.fraction_str(), frac.to_string());
1451 }
1452 assert_eq!(DicomTime::from_hms(9, 1, 1).unwrap().fraction_micro(), None);
1454 }
1455
1456 #[test]
1457 fn test_dicom_date_time_now_local() {
1458 let dicom_datetime_local = DicomDateTime::now_local()
1459 .expect("Failed to get current local datetime from DicomDateTime::now_local()");
1460 let dicom_datetime_local_time = dicom_datetime_local
1461 .time()
1462 .expect("Failed to get time from DicomDateTime");
1463 let dicom_datetime_local_date = dicom_datetime_local.date();
1464
1465 let system_time_local = Local::now().naive_local().time();
1466 let dicom_naive_time_local = dicom_datetime_local_time
1467 .to_naive_time()
1468 .expect("Failed to convert DicomDateTime time component to NaiveTime");
1469 let time_difference_local = system_time_local - dicom_naive_time_local;
1470 assert!(
1471 time_difference_local.abs() < Duration::seconds(1),
1472 "Time component difference between system and DicomDateTime local exceeds 1 second: {:?}",
1473 time_difference_local
1474 );
1475
1476 let system_date_local = Local::now().naive_local().date();
1477 let dicom_naive_date_local = dicom_datetime_local_date
1478 .to_naive_date()
1479 .expect("Failed to convert DicomDateTime date component to NaiveDate");
1480 assert_eq!(
1481 dicom_naive_date_local, system_date_local,
1482 "Date component mismatch between DicomDateTime and system"
1483 );
1484 }
1485
1486 #[test]
1487 fn test_dicom_date_time_now_utc() {
1488 let dicom_datetime_utc = DicomDateTime::now_utc()
1489 .expect("Failed to get current UTC datetime from DicomDateTime::now_utc()");
1490 let dicom_datetime_utc_time = dicom_datetime_utc
1491 .time()
1492 .expect("Failed to get time from DicomDateTime (UTC)");
1493 let dicom_datetime_utc_date = dicom_datetime_utc.date();
1494
1495 let system_time_utc = Utc::now().naive_utc().time();
1496 let dicom_naive_time_utc = dicom_datetime_utc_time
1497 .to_naive_time()
1498 .expect("Failed to convert DicomDateTime UTC time component to NaiveTime");
1499 let time_difference_utc = system_time_utc - dicom_naive_time_utc;
1500 assert!(
1501 time_difference_utc.abs() < Duration::seconds(1),
1502 "Time component difference between system and DicomDateTime UTC exceeds 1 second: {:?}",
1503 time_difference_utc
1504 );
1505
1506 let system_date_utc = Utc::now().naive_utc().date();
1507 let dicom_naive_date_utc = dicom_datetime_utc_date
1508 .to_naive_date()
1509 .expect("Failed to convert DicomDateTime UTC date component to NaiveDate");
1510 assert_eq!(
1511 dicom_naive_date_utc, system_date_utc,
1512 "Date component mismatch between DicomDateTime UTC and system"
1513 );
1514 }
1515
1516 #[test]
1517 fn test_dicom_datetime() {
1518 let default_offset = FixedOffset::east_opt(0).unwrap();
1519 assert_eq!(
1520 DicomDateTime::from_date_with_time_zone(
1521 DicomDate::from_ymd(2020, 2, 29).unwrap(),
1522 default_offset
1523 ),
1524 DicomDateTime {
1525 date: DicomDate::from_ymd(2020, 2, 29).unwrap(),
1526 time: None,
1527 time_zone: Some(default_offset)
1528 }
1529 );
1530
1531 assert_eq!(
1532 DicomDateTime::from_date(DicomDate::from_ym(2020, 2).unwrap())
1533 .earliest()
1534 .unwrap(),
1535 PreciseDateTime::Naive(NaiveDateTime::new(
1536 NaiveDate::from_ymd_opt(2020, 2, 1).unwrap(),
1537 NaiveTime::from_hms_micro_opt(0, 0, 0, 0).unwrap()
1538 ))
1539 );
1540
1541 assert_eq!(
1542 DicomDateTime::from_date_with_time_zone(
1543 DicomDate::from_ym(2020, 2).unwrap(),
1544 default_offset
1545 )
1546 .latest()
1547 .unwrap(),
1548 PreciseDateTime::TimeZone(
1549 FixedOffset::east_opt(0)
1550 .unwrap()
1551 .from_local_datetime(&NaiveDateTime::new(
1552 NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1553 NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1554 ))
1555 .unwrap()
1556 )
1557 );
1558
1559 assert_eq!(
1560 DicomDateTime::from_date_and_time_with_time_zone(
1561 DicomDate::from_ymd(2020, 2, 29).unwrap(),
1562 DicomTime::from_hmsf(23, 59, 59, 10, 2).unwrap(),
1563 default_offset
1564 )
1565 .unwrap()
1566 .earliest()
1567 .unwrap(),
1568 PreciseDateTime::TimeZone(
1569 FixedOffset::east_opt(0)
1570 .unwrap()
1571 .from_local_datetime(&NaiveDateTime::new(
1572 NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1573 NaiveTime::from_hms_micro_opt(23, 59, 59, 100_000).unwrap()
1574 ))
1575 .unwrap()
1576 )
1577 );
1578 assert_eq!(
1579 DicomDateTime::from_date_and_time_with_time_zone(
1580 DicomDate::from_ymd(2020, 2, 29).unwrap(),
1581 DicomTime::from_hmsf(23, 59, 59, 10, 2).unwrap(),
1582 default_offset
1583 )
1584 .unwrap()
1585 .latest()
1586 .unwrap(),
1587 PreciseDateTime::TimeZone(
1588 FixedOffset::east_opt(0)
1589 .unwrap()
1590 .from_local_datetime(&NaiveDateTime::new(
1591 NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1592 NaiveTime::from_hms_micro_opt(23, 59, 59, 109_999).unwrap()
1593 ))
1594 .unwrap()
1595 )
1596 );
1597
1598 assert_eq!(
1599 DicomDateTime::try_from(
1600 &FixedOffset::east_opt(0)
1601 .unwrap()
1602 .from_local_datetime(&NaiveDateTime::new(
1603 NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1604 NaiveTime::from_hms_micro_opt(23, 59, 59, 999_999).unwrap()
1605 ))
1606 .unwrap()
1607 )
1608 .unwrap(),
1609 DicomDateTime {
1610 date: DicomDate::from_ymd(2020, 2, 29).unwrap(),
1611 time: Some(DicomTime::from_hms_micro(23, 59, 59, 999_999).unwrap()),
1612 time_zone: Some(default_offset)
1613 }
1614 );
1615
1616 assert_eq!(
1617 DicomDateTime::try_from(
1618 &FixedOffset::east_opt(0)
1619 .unwrap()
1620 .from_local_datetime(&NaiveDateTime::new(
1621 NaiveDate::from_ymd_opt(2020, 2, 29).unwrap(),
1622 NaiveTime::from_hms_micro_opt(23, 59, 59, 0).unwrap()
1623 ))
1624 .unwrap()
1625 )
1626 .unwrap(),
1627 DicomDateTime {
1628 date: DicomDate::from_ymd(2020, 2, 29).unwrap(),
1629 time: Some(DicomTime::from_hms_micro(23, 59, 59, 0).unwrap()),
1630 time_zone: Some(default_offset)
1631 }
1632 );
1633
1634 assert_eq!(
1636 DicomDateTime::try_from(
1637 &FixedOffset::east_opt(0)
1638 .unwrap()
1639 .from_local_datetime(&NaiveDateTime::new(
1640 NaiveDate::from_ymd_opt(2023, 12, 31).unwrap(),
1641 NaiveTime::from_hms_micro_opt(23, 59, 59, 1_000_000).unwrap()
1642 ))
1643 .unwrap()
1644 )
1645 .unwrap(),
1646 DicomDateTime {
1647 date: DicomDate::from_ymd(2023, 12, 31).unwrap(),
1648 time: Some(DicomTime::from_hms_micro(23, 59, 60, 0).unwrap()),
1649 time_zone: Some(default_offset)
1650 }
1651 );
1652
1653 let dt: DicomDateTime = "20240229235959.123456+0000".parse().unwrap();
1656 assert_eq!(
1657 dt,
1658 DicomDateTime {
1659 date: DicomDate::from_ymd(2024, 2, 29).unwrap(),
1660 time: Some(DicomTime::from_hms_micro(23, 59, 59, 123_456).unwrap()),
1661 time_zone: Some(default_offset)
1662 }
1663 );
1664 assert!(dt.is_precise());
1665
1666 assert!(matches!(
1669 DicomDateTime::from_date_with_time_zone(
1670 DicomDate::from_ymd(2021, 2, 29).unwrap(),
1671 default_offset
1672 )
1673 .earliest(),
1674 Err(crate::value::range::Error::InvalidDate { .. })
1675 ));
1676
1677 assert!(matches!(
1678 DicomDateTime::from_date_and_time_with_time_zone(
1679 DicomDate::from_ym(2020, 2).unwrap(),
1680 DicomTime::from_hms_milli(23, 59, 59, 999).unwrap(),
1681 default_offset
1682 ),
1683 Err(Error::DateTimeFromPartials {
1684 value: DateComponent::Month,
1685 ..
1686 })
1687 ));
1688 assert!(matches!(
1689 DicomDateTime::from_date_and_time_with_time_zone(
1690 DicomDate::from_y(1).unwrap(),
1691 DicomTime::from_hms_micro(23, 59, 59, 10).unwrap(),
1692 default_offset
1693 ),
1694 Err(Error::DateTimeFromPartials {
1695 value: DateComponent::Year,
1696 ..
1697 })
1698 ));
1699
1700 assert!(matches!(
1701 DicomDateTime::from_date_and_time_with_time_zone(
1702 DicomDate::from_ymd(2000, 1, 1).unwrap(),
1703 DicomTime::from_hms_milli(23, 59, 59, 10).unwrap(),
1704 default_offset
1705 )
1706 .unwrap()
1707 .exact(),
1708 Err(crate::value::range::Error::ImpreciseValue { .. })
1709 ));
1710
1711 assert!(
1713 !DicomDateTime::from_date_and_time(
1714 DicomDate::from_ymd(2000, 1, 1).unwrap(),
1715 DicomTime::from_hms_milli(23, 59, 59, 10).unwrap()
1716 )
1717 .unwrap()
1718 .is_precise()
1719 );
1720
1721 assert!(
1722 DicomDateTime::from_date_and_time(
1723 DicomDate::from_ymd(2000, 1, 1).unwrap(),
1724 DicomTime::from_hms_micro(23, 59, 59, 654_321).unwrap()
1725 )
1726 .unwrap()
1727 .is_precise()
1728 );
1729 }
1730}