Skip to content

Commit e3c53b8

Browse files
authored
Merge pull request #675 from dbic/bf-strptime-subsecond
Make .subsecond optional in BIDS/DICOM datetime entries
2 parents 3c37619 + b810958 commit e3c53b8

File tree

4 files changed

+59
-9
lines changed

4 files changed

+59
-9
lines changed

heudiconv/bids.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from collections import OrderedDict
88
import csv
9-
from datetime import datetime
109
import errno
1110
from glob import glob
1211
import hashlib
@@ -32,6 +31,7 @@
3231
remove_suffix,
3332
save_json,
3433
set_readonly,
34+
strptime_micr,
3535
update_json,
3636
)
3737

@@ -369,7 +369,9 @@ def tuneup_bids_json_files(json_files: list[str]) -> None:
369369
set_readonly(json_phasediffname)
370370

371371

372-
def add_participant_record(studydir: str, subject: str, age: str | None, sex: str | None) -> None:
372+
def add_participant_record(
373+
studydir: str, subject: str, age: str | None, sex: str | None
374+
) -> None:
373375
participants_tsv = op.join(studydir, "participants.tsv")
374376
participant_id = "sub-%s" % subject
375377

@@ -945,17 +947,17 @@ def select_fmap_from_compatible_groups(
945947
k for k, v in acq_times_fmaps.items() if v == first_acq_time
946948
][0]
947949
elif criterion == "Closest":
948-
json_acq_time = datetime.strptime(
950+
json_acq_time = strptime_micr(
949951
acq_times[
950952
# remove session folder and '.json', add '.nii.gz':
951953
remove_suffix(remove_prefix(json_file, sess_folder + op.sep), ".json")
952954
+ ".nii.gz"
953955
],
954-
"%Y-%m-%dT%H:%M:%S.%f",
956+
"%Y-%m-%dT%H:%M:%S[.%f]",
955957
)
956958
# differences in acquisition time (abs value):
957959
diff_fmaps_acq_times = {
958-
k: abs(datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%f") - json_acq_time)
960+
k: abs(strptime_micr(v, "%Y-%m-%dT%H:%M:%S[.%f]") - json_acq_time)
959961
for k, v in acq_times_fmaps.items()
960962
}
961963
min_diff_acq_times = sorted(diff_fmaps_acq_times.values())[0]

heudiconv/dicoms.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@
1515

1616
import pydicom as dcm
1717

18-
from .utils import SeqInfo, TempDirs, get_typed_attr, load_json, set_readonly
18+
from .utils import (
19+
SeqInfo,
20+
TempDirs,
21+
get_typed_attr,
22+
load_json,
23+
set_readonly,
24+
strptime_micr,
25+
)
1926

2027
if TYPE_CHECKING:
2128
if sys.version_info >= (3, 8):
@@ -481,16 +488,16 @@ def get_datetime_from_dcm(dcm_data: dcm.FileDataset) -> Optional[datetime.dateti
481488
acq_date = dcm_data.get("AcquisitionDate")
482489
acq_time = dcm_data.get("AcquisitionTime")
483490
if not (acq_date is None or acq_time is None):
484-
return datetime.datetime.strptime(acq_date + acq_time, "%Y%m%d%H%M%S.%f")
491+
return strptime_micr(acq_date + acq_time, "%Y%m%d%H%M%S[.%f]")
485492

486493
acq_dt = dcm_data.get("AcquisitionDateTime")
487494
if acq_dt is not None:
488-
return datetime.datetime.strptime(acq_dt, "%Y%m%d%H%M%S.%f")
495+
return strptime_micr(acq_dt, "%Y%m%d%H%M%S[.%f]")
489496

490497
series_date = dcm_data.get("SeriesDate")
491498
series_time = dcm_data.get("SeriesTime")
492499
if not (series_date is None or series_time is None):
493-
return datetime.datetime.strptime(series_date + series_time, "%Y%m%d%H%M%S.%f")
500+
return strptime_micr(series_date + series_time, "%Y%m%d%H%M%S[.%f]")
494501
return None
495502

496503

heudiconv/tests/test_utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from datetime import datetime
34
import json
45
from json.decoder import JSONDecodeError
56
import os
@@ -21,6 +22,7 @@
2122
remove_prefix,
2223
remove_suffix,
2324
save_json,
25+
strptime_micr,
2426
update_json,
2527
)
2628

@@ -168,6 +170,24 @@ def test_get_datetime() -> None:
168170
)
169171

170172

173+
@pytest.mark.parametrize(
174+
"dt, fmt",
175+
[
176+
("20230310190100", "%Y%m%d%H%M%S"),
177+
("2023-04-02T11:47:09", "%Y-%m-%dT%H:%M:%S"),
178+
],
179+
)
180+
def test_strptime_micr(dt: str, fmt: str) -> None:
181+
target = datetime.strptime(dt, fmt)
182+
assert strptime_micr(dt, fmt) == target
183+
assert strptime_micr(dt, fmt + "[.%f]") == target
184+
assert strptime_micr(dt + ".0", fmt + "[.%f]") == target
185+
assert strptime_micr(dt + ".000000", fmt + "[.%f]") == target
186+
assert strptime_micr(dt + ".1", fmt + "[.%f]") == datetime.strptime(
187+
dt + ".1", fmt + ".%f"
188+
)
189+
190+
171191
def test_remove_suffix() -> None:
172192
"""
173193
Test utils.remove_suffix()

heudiconv/utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,27 @@ def get_datetime(date: str, time: str, *, microseconds: bool = True) -> str:
666666
return datetime_str
667667

668668

669+
def strptime_micr(date_string: str, fmt: str) -> datetime:
670+
r"""
671+
Decorate strptime while supporting optional [.%f] in the format at the end
672+
673+
Parameters
674+
----------
675+
date_string: str
676+
Date string to parse
677+
fmt: str
678+
Format string. If it ends with [.%f], we keep it if date_string ends with
679+
'.\d+' regex and not if it does not.
680+
"""
681+
682+
optional_micr = "[.%f]"
683+
if fmt.endswith(optional_micr):
684+
fmt = fmt[: -len(optional_micr)]
685+
if re.search(r"\.\d+$", date_string):
686+
fmt += ".%f"
687+
return datetime.strptime(date_string, fmt)
688+
689+
669690
def remove_suffix(s: str, suf: str) -> str:
670691
"""
671692
Remove suffix from the end of the string

0 commit comments

Comments
 (0)