Repository: mne-tools/mne-python

Issue:
raw object meas_date is moderated by time zone of the computer reading the EEG file (from an EGI MFF file).
#### Describe the bug
the `Info['meas_date']` `datetime` object derived from a EGI Net station MFF file (continuous recording), appears to be moderated by the time zone of the computer which read in the EGI file. `info['meas_date']` should only be concerned with the timestamp and timezone indicated in the MFF file, and should be completely agnostic to the timezone of the computer reading in the MFF file.

#### Steps to reproduce
- I have imported the same EGI MFF file using the same code (`mne.io.read_raw_egi()`) on two separate computers, which we'll call **computer 1** and **computer 2**.
-See the two attached PDF's, for the code and output for each of the two computers
- The computers are on the same time zone (EST), but **Computer 1** is expressed in  Universal time (UTC) and **computer 2** is expressed in local time (EST).
- FYI that the MFF file was recorded in San Diego (PST).
- On **computer 1**, raw.info['meas_date'] returns `datetime.datetime(2019, 10, 29, 12, 41, 1, tzinfo=datetime.timezone.utc)`, indicating that the recording start time was **12:41pm**.
- On **computer 2**, `raw.info['meas_date']` returns `datetime.datetime(2019, 10, 29, 16, 41, 1, tzinfo=datetime.timezone.utc)`, indicating that the recording start time was **4:41pm**.
- The `datetime` returned by **computer 1** is the correct recording start time, so it would seem **computer 2** being set to local time caused the issue. Regardless, `info['meas_date']` should be completely agnostic to the timezone of the computer reading in the MFF file. 
- As far as I can tell, the same version of mne is used on both computers.
- I can share the MFF file used  for this report, upon request.

#### Expected results
`datetime.datetime(2019, 10, 29, 12, 41, 1, tzinfo=datetime.timezone.utc)`

Which was returned by **Computer 1**, and  I have confirmed that this is the correct start time of the recording (12:41pm)

![Computer1](https://user-images.githubusercontent.com/52462026/151207194-6408ebd1-f847-4988-b203-88b9f1299f77.png)


#### Actual results
`datetime.datetime(2019, 10, 29, 16, 41, 1, tzinfo=datetime.timezone.utc)`

Which was returned by **Computer 2**, indicating that the recording start time was at 4:41pm, which is incorrect.
![computer2](https://user-images.githubusercontent.com/52462026/151207265-3f0d5882-9b8e-4ecb-bee0-6b0d1f8fa1f8.png)


#### Additional information
**Computer 1**
Platform:       Linux-3.10.0-1160.36.2.el7.x86_64-x86_64-with-centos-7.9.2009-Core
Python:         3.7.4 (default, Jul 18 2019, 19:34:02)  [GCC 5.4.0]
Executable:     /cvmfs/soft.computecanada.ca/easybuild/software/2017/Core/python/3.7.4/bin/python
CPU:            : 24 cores
Memory:         Unavailable (requires "psutil" package)
mne:            0.24.1
numpy:          1.20.2 {blas=mkl_rt, lapack=mkl_rt}
scipy:          1.4.1
matplotlib:     3.2.1 {backend=module://ipykernel.pylab.backend_inline}

**Computer 2**
Platform:       Linux-4.18.0-305.25.1.el8_4.x86_64-x86_64-with-glibc2.2.5
Python:         3.8.10 (default, Jun 16 2021, 14:20:20)  [GCC 9.3.0]
Executable:     /home/shuberty/virtual_envs/pylossless/bin/python
CPU:            : 64 cores
Memory:         Unavailable (requires "psutil" package)
mne:            0.24.1
numpy:          1.21.2 {blas=/cvmfs/soft.computecanada.ca/easybuild/software/2020/Core/flexiblas/3.0.4/include/flexiblas, lapack=/cvmfs/soft.computecanada.ca/easybuild/software/2020/Core/flexiblas/3.0.4/include/flexiblas}
scipy:          1.7.3
matplotlib:     3.4.2 {backend=module://matplotlib_inline.backend_inline}

[computer1_bugreport.pdf](https://github.com/mne-tools/mne-python/files/7943326/helios_bugreport.pdf)
[computer2_bugreport.pdf](https://github.com/mne-tools/mne-python/files/7943329/narval_bugreport.pdf)



Cheers,

Scott


Discussion:
Hello! 👋 Thanks for opening your first issue here! ❤️ We will try to get back to you soon. 🚴🏽‍♂️

This does seem like a bug -- `read_raw_egi` should detect any time zone offset, remove it to get to UTC, and save the actually used time zone in `info['utc_offset']`. Do you want to try fixing this? I think it would need to be around here:

https://github.com/mne-tools/mne-python/blob/b5688906f971ad0b4026d9812e021dbfd015bf70/mne/io/egi/egimff.py#L221-L222

and 

https://github.com/mne-tools/mne-python/blob/b5688906f971ad0b4026d9812e021dbfd015bf70/mne/io/egi/egimff.py#L456-L460
Sure, I can take a look at this in the next week or so, and will follow up.

Thanks for the help,

Scott

Resolving patch:
diff --git a/mne/io/egi/egimff.py b/mne/io/egi/egimff.py
--- a/mne/io/egi/egimff.py
+++ b/mne/io/egi/egimff.py
@@ -5,7 +5,6 @@
 import math
 import os.path as op
 import re
-import time
 from xml.dom.minidom import parse
 
 import numpy as np
@@ -211,24 +210,27 @@ def _read_header(input_fname):
     mff_hdr = _read_mff_header(input_fname)
     with open(input_fname + '/signal1.bin', 'rb') as fid:
         version = np.fromfile(fid, np.int32, 1)[0]
-    # This should be equivalent to the following, but no need for external dep:
-    # import dateutil.parser
-    # time_n = dateutil.parser.parse(mff_hdr['date'])
-    dt = mff_hdr['date'][:26]
-    assert mff_hdr['date'][-6] in ('+', '-')
-    sn = -1 if mff_hdr['date'][-6] == '-' else 1  # +
-    tz = [sn * int(t) for t in (mff_hdr['date'][-5:-3], mff_hdr['date'][-2:])]
-    time_n = datetime.datetime.strptime(dt, '%Y-%m-%dT%H:%M:%S.%f')
-    time_n = time_n.replace(tzinfo=_FixedOffset(60 * tz[0] + tz[1]))
+    '''
+    the datetime.strptime .f directive (milleseconds)
+    will only accept up to 6 digits. if there are more than
+    six millesecond digits in the provided timestamp string
+    (i.e. because of trailing zeros, as in test_egi_pns.mff)
+    then slice both the first 26 elements and the last 6
+    elements of the timestamp string to truncate the
+    milleseconds to 6 digits and extract the timezone,
+    and then piece these together and assign back to mff_hdr['date']
+    '''
+    if len(mff_hdr['date']) > 32:
+        dt, tz = [mff_hdr['date'][:26], mff_hdr['date'][-6:]]
+        mff_hdr['date'] = dt + tz
+
+    time_n = (datetime.datetime.strptime(
+              mff_hdr['date'], '%Y-%m-%dT%H:%M:%S.%f%z'))
+
     info = dict(
         version=version,
-        year=int(time_n.strftime('%Y')),
-        month=int(time_n.strftime('%m')),
-        day=int(time_n.strftime('%d')),
-        hour=int(time_n.strftime('%H')),
-        minute=int(time_n.strftime('%M')),
-        second=int(time_n.strftime('%S')),
-        millisecond=int(time_n.strftime('%f')),
+        meas_dt_local=time_n,
+        utc_offset=time_n.strftime('%z'),
         gain=0,
         bits=0,
         value_range=0)
@@ -452,12 +454,12 @@ def __init__(self, input_fname, eog=None, misc=None,
             self.event_id = None
             egi_info['new_trigger'] = None
             event_codes = []
+
+        meas_dt_utc = (egi_info['meas_dt_local']
+                       .astimezone(datetime.timezone.utc))
         info = _empty_info(egi_info['sfreq'])
-        my_time = datetime.datetime(
-            egi_info['year'], egi_info['month'], egi_info['day'],
-            egi_info['hour'], egi_info['minute'], egi_info['second'])
-        my_timestamp = time.mktime(my_time.timetuple())
-        info['meas_date'] = _ensure_meas_date_none_or_dt((my_timestamp, 0))
+        info['meas_date'] = _ensure_meas_date_none_or_dt(meas_dt_utc)
+        info['utc_offset'] = egi_info['utc_offset']
         info['device_info'] = dict(type=egi_info['device'])
 
         # First: EEG


Test patch:
diff --git a/mne/io/egi/tests/test_egi.py b/mne/io/egi/tests/test_egi.py
--- a/mne/io/egi/tests/test_egi.py
+++ b/mne/io/egi/tests/test_egi.py
@@ -7,6 +7,7 @@
 import os.path as op
 import os
 import shutil
+from datetime import datetime, timezone
 
 import numpy as np
 from numpy.testing import assert_array_equal, assert_allclose
@@ -435,3 +436,23 @@ def test_egi_coord_frame():
             assert_allclose(loc[1:], 0, atol=1e-7, err_msg='RPA')
     for d in info['dig'][3:]:
         assert d['kind'] == FIFF.FIFFV_POINT_EEG
+
+
+@requires_testing_data
+@pytest.mark.parametrize('fname, timestamp, utc_offset', [
+    (egi_mff_fname, '2017-02-23T11:35:13.220824+01:00', '+0100'),
+    (egi_mff_pns_fname, '2017-09-20T09:55:44.072000+01:00', '+0100'),
+    (egi_eprime_pause_fname, '2018-07-30T10:46:09.621673-04:00', '-0400'),
+    (egi_pause_w1337_fname, '2019-10-14T10:54:27.395210-07:00', '-0700'),
+])
+def test_meas_date(fname, timestamp, utc_offset):
+    """Test meas date conversion."""
+    raw = read_raw_egi(fname, verbose='warning')
+    dt = datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%f%z')
+    measdate = dt.astimezone(timezone.utc)
+    hour_local = int(dt.strftime('%H'))
+    hour_utc = int(raw.info['meas_date'].strftime('%H'))
+    local_utc_diff = hour_local - hour_utc
+    assert raw.info['meas_date'] == measdate
+    assert raw.info['utc_offset'] == utc_offset
+    assert local_utc_diff == int(utc_offset[:-2])