4
4
from collections .abc import Callable
5
5
from collections .abc import Mapping as MappingABC
6
6
import copy
7
- from datetime import datetime
7
+ import datetime
8
8
from glob import glob
9
9
import hashlib
10
10
import json
35
35
cast ,
36
36
overload ,
37
37
)
38
+ import warnings
39
+
40
+ import pydicom as dcm
41
+ from pydicom .tag import TagType
38
42
39
43
lgr = logging .getLogger (__name__ )
40
44
@@ -662,13 +666,13 @@ def get_datetime(date: str, time: str, *, microseconds: bool = True) -> str:
662
666
# add dummy microseconds if not available for strptime to parse
663
667
time += ".000000"
664
668
td = time + ":" + date
665
- datetime_str = datetime .strptime (td , "%H%M%S.%f:%Y%m%d" ).isoformat ()
669
+ datetime_str = datetime .datetime . strptime (td , "%H%M%S.%f:%Y%m%d" ).isoformat ()
666
670
if not microseconds :
667
671
datetime_str = datetime_str .split ("." , 1 )[0 ]
668
672
return datetime_str
669
673
670
674
671
- def strptime_micr (date_string : str , fmt : str ) -> datetime :
675
+ def strptime_micr (date_string : str , fmt : str ) -> datetime . datetime :
672
676
r"""
673
677
Decorate strptime while supporting optional [.%f] in the format at the end
674
678
@@ -681,12 +685,156 @@ def strptime_micr(date_string: str, fmt: str) -> datetime:
681
685
'.\d+' regex and not if it does not.
682
686
"""
683
687
688
+ warnings .warn (
689
+ "strptime_micr() is deprecated, please use strptime() instead." ,
690
+ DeprecationWarning ,
691
+ stacklevel = 2 ,
692
+ )
684
693
optional_micr = "[.%f]"
685
694
if fmt .endswith (optional_micr ):
686
695
fmt = fmt [: - len (optional_micr )]
687
696
if re .search (r"\.\d+$" , date_string ):
688
697
fmt += ".%f"
689
- return datetime .strptime (date_string , fmt )
698
+ return datetime .datetime .strptime (date_string , fmt )
699
+
700
+
701
+ def datetime_utc_offset (
702
+ datetime_obj : datetime .datetime , utc_offset : str
703
+ ) -> datetime .datetime :
704
+ """set the datetime's tzinfo by parsing an utc offset string"""
705
+ # https://dicom.innolitics.com/ciods/electromyogram/sop-common/00080201
706
+ extract_offset = re .match (r"([+\-]?)(\d{2})(\d{2})" , utc_offset )
707
+ if extract_offset is None :
708
+ raise ValueError (f"utc offset { utc_offset } is not valid" )
709
+ sign , hours , minutes = extract_offset .groups ()
710
+ sign = - 1 if sign == "-" else 1
711
+ hours , minutes = int (hours ), int (minutes )
712
+ tzinfo = datetime .timezone (sign * datetime .timedelta (hours = hours , minutes = minutes ))
713
+ return datetime_obj .replace (tzinfo = tzinfo )
714
+
715
+
716
+ def strptime (datetime_string : str , fmts : list [str ]) -> datetime .datetime :
717
+ """
718
+ Try datetime.strptime on a list of formats returning the first successful attempt.
719
+
720
+ Parameters
721
+ ----------
722
+ datetime_string: str
723
+ Datetime string to parse
724
+ fmts: list[str]
725
+ List of format strings
726
+ """
727
+ datetime_str = datetime_string
728
+ for fmt in fmts :
729
+ try :
730
+ return datetime .datetime .strptime (datetime_str , fmt )
731
+ except ValueError :
732
+ pass
733
+ raise ValueError (f"Unable to parse datetime string: { datetime_str } " )
734
+
735
+
736
+ def strptime_bids (datetime_string : str ) -> datetime .datetime :
737
+ """
738
+ Create a datetime object from a bids datetime string.
739
+
740
+ Parameters
741
+ ----------
742
+ date_string: str
743
+ Datetime string to parse
744
+ """
745
+ # https://bids-specification.readthedocs.io/en/stable/common-principles.html#units
746
+ fmts = [
747
+ "%Y-%m-%dT%H:%M:%S.%f%z" ,
748
+ "%Y-%m-%dT%H:%M:%S%z" ,
749
+ "%Y-%m-%dT%H:%M:%S.%f" ,
750
+ "%Y-%m-%dT%H:%M:%S" ,
751
+ ]
752
+ datetime_obj = strptime (datetime_string , fmts )
753
+ return datetime_obj
754
+
755
+
756
+ def strptime_dcm_da_tm (
757
+ dcm_data : dcm .Dataset , da_tag : TagType , tm_tag : TagType
758
+ ) -> datetime .datetime :
759
+ """
760
+ Create a datetime object from a dicom DA tag and TM tag.
761
+
762
+ Parameters
763
+ ----------
764
+ dcm_data : dcm.Dataset
765
+ DICOM with header, e.g., as read by pydicom.dcmread.
766
+ da_tag: str
767
+ Dicom tag with DA value representation
768
+ tm_tag: str
769
+ Dicom tag with TM value representation
770
+ """
771
+ # https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
772
+ date_str = dcm_data [da_tag ].value
773
+ fmts = [
774
+ "%Y%m%d" ,
775
+ ]
776
+ date = strptime (date_str , fmts )
777
+
778
+ time_str = dcm_data [tm_tag ].value
779
+ fmts = ["%H" , "%H%M" , "%H%M%S" , "%H%M%S.%f" ]
780
+ time = strptime (time_str , fmts )
781
+
782
+ datetime_obj = datetime .datetime .combine (date .date (), time .time ())
783
+
784
+ if utc_offset_dcm := dcm_data .get ((0x0008 , 0x0201 )):
785
+ utc_offset = utc_offset_dcm .value
786
+ datetime_obj = (
787
+ datetime_utc_offset (datetime_obj , utc_offset )
788
+ if utc_offset
789
+ else datetime_obj
790
+ )
791
+ return datetime_obj
792
+
793
+
794
+ def strptime_dcm_dt (dcm_data : dcm .Dataset , dt_tag : TagType ) -> datetime .datetime :
795
+ """
796
+ Create a datetime object from a dicom DT tag.
797
+
798
+ Parameters
799
+ ----------
800
+ dcm_data : dcm.FileDataset
801
+ DICOM with header, e.g., as read by pydicom.dcmread.
802
+ Objects with __getitem__ and have those keys with values properly formatted may also work
803
+ da_tag: str
804
+ Dicom tag with DT value representation
805
+ """
806
+ # https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
807
+ datetime_str = dcm_data [dt_tag ].value
808
+ fmts = [
809
+ "%Y%z" ,
810
+ "%Y%m%z" ,
811
+ "%Y%m%d%z" ,
812
+ "%Y%m%d%H%z" ,
813
+ "%Y%m%d%H%M%z" ,
814
+ "%Y%m%d%H%M%S%z" ,
815
+ "%Y%m%d%H%M%S.%f%z" ,
816
+ "%Y" ,
817
+ "%Y%m" ,
818
+ "%Y%m%d" ,
819
+ "%Y%m%d%H" ,
820
+ "%Y%m%d%H%M" ,
821
+ "%Y%m%d%H%M%S" ,
822
+ "%Y%m%d%H%M%S.%f" ,
823
+ ]
824
+ datetime_obj = strptime (datetime_str , fmts )
825
+
826
+ if utc_offset_dcm := dcm_data .get ((0x0008 , 0x0201 )):
827
+ if utc_offset := utc_offset_dcm .value :
828
+ datetime_obj2 = datetime_utc_offset (datetime_obj , utc_offset )
829
+ if datetime_obj .tzinfo and datetime_obj2 != datetime_obj :
830
+ lgr .warning (
831
+ "Unexpectedly previously parsed datetime %s contains zoneinfo which is different from the one obtained from DICOMs UTFOffset field: %s" ,
832
+ datetime_obj ,
833
+ datetime_obj2 ,
834
+ )
835
+ else :
836
+ datetime_obj = datetime_obj2
837
+ return datetime_obj
690
838
691
839
692
840
def remove_suffix (s : str , suf : str ) -> str :
0 commit comments