diff --git a/heudiconv/bids.py b/heudiconv/bids.py index 6a9c136c..ce8ab4c3 100644 --- a/heudiconv/bids.py +++ b/heudiconv/bids.py @@ -21,10 +21,24 @@ json_dumps_pretty, set_readonly, is_readonly, + get_datetime, ) lgr = logging.getLogger(__name__) +# Fields to be populated in _scans files. Order matters +SCANS_FILE_FIELDS = OrderedDict([ + ("filename", OrderedDict([ + ("Description", "Name of the nifti file")])), + ("acq_time", OrderedDict([ + ("LongName", "Acquisition time"), + ("Description", "Acquisition time of the particular scan")])), + ("operator", OrderedDict([ + ("Description", "Name of the operator")])), + ("randstr", OrderedDict([ + ("LongName", "Random string"), + ("Description", "md5 hash of UIDs")])), +]) class BIDSError(Exception): pass @@ -359,22 +373,9 @@ def add_rows_to_scans_keys_file(fn, newrows): # _scans.tsv). This auto generation will make BIDS-validator happy. scans_json = '.'.join(fn.split('.')[:-1] + ['json']) if not op.lexists(scans_json): - save_json(scans_json, - OrderedDict([ - ("filename", OrderedDict([ - ("Description", "Name of the nifti file")])), - ("acq_time", OrderedDict([ - ("LongName", "Acquisition time"), - ("Description", "Acquisition time of the particular scan")])), - ("operator", OrderedDict([ - ("Description", "Name of the operator")])), - ("randstr", OrderedDict([ - ("LongName", "Random string"), - ("Description", "md5 hash of UIDs")])), - ]), - sort_keys=False) + save_json(scans_json, SCANS_FILE_FIELDS, sort_keys=False) - header = ['filename', 'acq_time', 'operator', 'randstr'] + header = SCANS_FILE_FIELDS # prepare all the data rows data_rows = [[k] + v for k, v in fnames2info.items()] # sort by the date/filename @@ -406,9 +407,8 @@ def get_formatted_scans_key_row(dcm_fn): # parse date and time and get it into isoformat try: date = dcm_data.ContentDate - time = dcm_data.ContentTime.split('.')[0] - td = time + date - acq_time = datetime.strptime(td, '%H%M%S%Y%m%d').isoformat() + time = dcm_data.ContentTime + acq_time = get_datetime(date, time) except (AttributeError, ValueError) as exc: lgr.warning("Failed to get date/time for the content: %s", str(exc)) acq_time = '' diff --git a/heudiconv/tests/test_heuristics.py b/heudiconv/tests/test_heuristics.py index f36bbb4c..eedf61f2 100644 --- a/heudiconv/tests/test_heuristics.py +++ b/heudiconv/tests/test_heuristics.py @@ -116,7 +116,7 @@ def test_scans_keys_reproin(tmpdir, invocation): if i != 0: assert(os.path.exists(pjoin(dirname(scans_keys[0]), row[0]))) assert(re.match( - '^[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}$', + '^[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}.[\d]{6}$', row[1])) diff --git a/heudiconv/tests/test_main.py b/heudiconv/tests/test_main.py index 91291378..ab1dd73d 100644 --- a/heudiconv/tests/test_main.py +++ b/heudiconv/tests/test_main.py @@ -173,7 +173,7 @@ def test_get_formatted_scans_key_row(): row1 = get_formatted_scans_key_row(dcm_fn) assert len(row1) == 3 - assert row1[0] == '2016-10-14T09:26:36' + assert row1[0] == '2016-10-14T09:26:36.693000' assert row1[1] == 'n/a' prandstr1 = row1[2] diff --git a/heudiconv/tests/test_utils.py b/heudiconv/tests/test_utils.py index 00f8a0a2..b8c8c160 100644 --- a/heudiconv/tests/test_utils.py +++ b/heudiconv/tests/test_utils.py @@ -10,6 +10,7 @@ load_json, create_tree, save_json, + get_datetime, JSONDecodeError) import pytest @@ -85,3 +86,12 @@ def test_load_json(tmpdir, caplog): save_json(valid_json_file, vcontent) assert load_json(valid_json_file) == vcontent + + +def test_get_datetime(): + """ + Test utils.get_datetime() + """ + assert get_datetime('20200512', '162130') == '2020-05-12T16:21:30' + assert get_datetime('20200512', '162130.5') == '2020-05-12T16:21:30.500000' + assert get_datetime('20200512', '162130.5', microseconds=False) == '2020-05-12T16:21:30' diff --git a/heudiconv/utils.py b/heudiconv/utils.py index da3ebc56..f2631757 100644 --- a/heudiconv/utils.py +++ b/heudiconv/utils.py @@ -13,6 +13,7 @@ from collections import namedtuple from glob import glob from subprocess import check_output +from datetime import datetime from nipype.utils.filemanip import which @@ -505,3 +506,33 @@ def get_typed_attr(obj, attr, _type, default=None): except (TypeError, ValueError): return default return val + + +def get_datetime(date, time, *, microseconds=True): + """ + Combine date and time from dicom to isoformat. + + Parameters + ---------- + date : str + Date in YYYYMMDD format. + time : str + Time in either HHMMSS.ffffff format or HHMMSS format. + microseconds: bool, optional + Either to include microseconds in the output + + Returns + ------- + datetime_str : str + Combined date and time in ISO format, with microseconds as + if fraction was provided in 'time', and 'microseconds' was + True. + """ + if '.' not in time: + # add dummy microseconds if not available for strptime to parse + time += '.000000' + td = time + ':' + date + datetime_str = datetime.strptime(td, '%H%M%S.%f:%Y%m%d').isoformat() + if not microseconds: + datetime_str = datetime_str.split('.', 1)[0] + return datetime_str