Skip to content

Commit

Permalink
account for optional Z in time strs (#1240)
Browse files Browse the repository at this point in the history
* account for optional Z in time strs

* Try to handle UTC zero offset and local time zones correctly

* Fix comment

* Fix

* Better variable name

* Add tests

* Update mne_bids/read.py

* Add changelog entry

---------

Co-authored-by: Richard Höchenberger <[email protected]>
  • Loading branch information
sappelhoff and hoechenberger authored Mar 25, 2024
1 parent 17d20c1 commit c75ac58
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 7 deletions.
2 changes: 2 additions & 0 deletions doc/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ Detailed list of changes
``pandas.Int64Dtype`` instead of ``float64``, by `Eric Larson`_ (:gh:`1227`)
- The :func:`mne_bids.copyfiles.copyfile_ctf` now accounts for files with ``.{integer}_meg4`` extension, instead of only .meg4,
when renaming the files of a .ds folder, by `Mara Wolter`_ (:gh:`1230`)
- We fixed handling of time zones when reading ``*_scans.tsv`` files; specifically, non-UTC timestamps are now processed correctly,
by `Stefan Appelhoff`_ and `Richard Höchenberger`_ (:gh:`1240`)

⚕️ Code health
^^^^^^^^^^^^^^
Expand Down
30 changes: 25 additions & 5 deletions mne_bids/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,12 +327,32 @@ def _handle_scans_reading(scans_fname, raw, bids_path):
# extract the acquisition time from scans file
acq_time = acq_times[row_ind]
if acq_time != "n/a":
# microseconds in the acquisition time is optional
# BIDS allows the time to be stored in UTC with a zero time-zone offset, as
# indicated by a trailing "Z" in the datetime string. If the "Z" is missing, the
# time is represented as "local" time. We have no way to know what the local
# time zone is at the *acquisition* site; so we simply assume the same time zone
# as the user's current system (this is what the spec demands anyway).
acq_time_is_utc = acq_time.endswith("Z")

# microseconds part in the acquisition time is optional; add it if missing
if "." not in acq_time:
# acquisition time ends with '.%fZ' microseconds string
acq_time += ".0Z"
acq_time = datetime.strptime(acq_time, "%Y-%m-%dT%H:%M:%S.%fZ")
acq_time = acq_time.replace(tzinfo=timezone.utc)
if acq_time_is_utc:
acq_time = acq_time.replace("Z", ".0Z")
else:
acq_time += ".0"

date_format = "%Y-%m-%dT%H:%M:%S.%f"
if acq_time_is_utc:
date_format += "Z"

acq_time = datetime.strptime(acq_time, date_format)

if acq_time_is_utc:
# Enforce setting timezone to UTC without additonal conversion
acq_time = acq_time.replace(tzinfo=timezone.utc)
else:
# Convert time offset to UTC
acq_time = acq_time.astimezone(timezone.utc)

logger.debug(
f"Loaded {scans_fname} scans file to set " f"acq_time as {acq_time}."
Expand Down
22 changes: 20 additions & 2 deletions mne_bids/tests/test_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,20 +601,38 @@ def test_handle_scans_reading(tmp_path):
acq_time_str = scans_tsv["acq_time"][0]
acq_time = datetime.strptime(acq_time_str, "%Y-%m-%dT%H:%M:%S.%fZ")
acq_time = acq_time.replace(tzinfo=timezone.utc)
new_acq_time = acq_time_str.split(".")[0]
new_acq_time = acq_time_str.split(".")[0] + "Z"
assert acq_time == raw_01.info["meas_date"]
scans_tsv["acq_time"][0] = new_acq_time
_to_tsv(scans_tsv, scans_path)

# now re-load the data and it should be different
# from the original date and the same as the newly altered date
raw_02 = read_raw_bids(bids_path)
new_acq_time += ".0Z"
new_acq_time = new_acq_time.replace("Z", ".0Z")
new_acq_time = datetime.strptime(new_acq_time, "%Y-%m-%dT%H:%M:%S.%fZ")
new_acq_time = new_acq_time.replace(tzinfo=timezone.utc)
assert raw_02.info["meas_date"] == new_acq_time
assert new_acq_time != raw_01.info["meas_date"]

# Test without optional zero-offset UTC time-zone indicator (i.e., without trailing
# "Z")
for has_microsecs in (True, False):
new_acq_time_str = "2002-12-03T19:01:10"
date_format = "%Y-%m-%dT%H:%M:%S"
if has_microsecs:
new_acq_time_str += ".0"
date_format += ".%f"

scans_tsv["acq_time"][0] = new_acq_time_str
_to_tsv(scans_tsv, scans_path)

# now re-load the data and it should be different
# from the original date and the same as the newly altered date
raw_03 = read_raw_bids(bids_path)
new_acq_time = datetime.strptime(new_acq_time_str, date_format)
assert raw_03.info["meas_date"] == new_acq_time.astimezone(timezone.utc)


@pytest.mark.filterwarnings(warning_str["channel_unit_changed"])
def test_handle_scans_reading_brainvision(tmp_path):
Expand Down

0 comments on commit c75ac58

Please sign in to comment.