Repository: mne-tools/mne-python

Issue:
Read EDF files with dummy zero year/month/day
#### Describe the bug
EDF file that has dummy year/month/day of 0's cannot be read by mne.io.read_raw_edf() and exits with error "month must be in 1..12"

#### Expected results
I except the code still be able to read the data in the .edf file, and I would suggest that another dummy date is used instead of the 0's in the file, i propose using 1900-01-01.

Current code in mne/io/edf/edf.py:
```
# Recording ID
        meas_id = {}
        meas_id['recording_id'] = fid.read(80).decode('latin-1').strip(' \x00')

        day, month, year = [int(x) for x in
                            re.findall(r'(\d+)', fid.read(8).decode())]
        hour, minute, sec = [int(x) for x in
                             re.findall(r'(\d+)', fid.read(8).decode())]
        century = 2000 if year < 50 else 1900

```
Proposal (same as above, only an additional if clause):
```
# Recording ID
        meas_id = {}
        meas_id['recording_id'] = fid.read(80).decode('latin-1').strip(' \x00')

        day, month, year = [int(x) for x in
                            re.findall(r'(\d+)', fid.read(8).decode())]
        hour, minute, sec = [int(x) for x in
                             re.findall(r'(\d+)', fid.read(8).decode())]
        century = 2000 if year < 50 else 1900

        if any([day == 0, month == 0, year == 0]):
            year = 1900
            month = 1
            day = 1

```


If you think this is a solution you'd accept, I can also do a Pull request, although I am happy to hand it over to someone else too, as it is just a simple if clause add-on.

Best,
Wolfgang



Discussion:
I'm fine with this change - @wolfgang-ganglberger it would be great if you submitted a PR. I suggest to also issue a warning if a date component is zero, e.g.

```python
        if any([day == 0, month == 0, year == 0]):
            warn('Invalid date encountered ({:04d}-{:02d}-{:02d}), setting it '
                 'to 1900-01-01.'.format(year, month, day))
            year, month, day = 1900, 1, 1
```
Since MNE 0.21 now requires Python >= 3.6, you could also use an f-string:

```python
            warn(f'Invalid date ({year:04d}-{month:02d}-{day:02d}), setting '
                 f'it to 1900-01-01.')
```
+1 for a PR but it should deal with any date formatting pb not just 0
placeholders

>

Agreed, but this just means checking if the values are within valid ranges, because we already convert to int after reading from the header. So I guess checking `1 <= day <= 31`, `1 <= month <= 12`, and `year >= 1900` (or some other year) would be sufficient, or do you want a true check for a valid date (e.g. is February 29 2017 valid)?
I would use a try trying to construct a datetime


Resolving patch:
diff --git a/doc/sphinxext/gh_substitutions.py b/doc/sphinxext/gh_substitutions.py
--- a/doc/sphinxext/gh_substitutions.py
+++ b/doc/sphinxext/gh_substitutions.py
@@ -18,7 +18,7 @@ def gh_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
         slug = text
     else:
         slug = 'issues/' + text
-    text = 'gh-' + text
+    text = '#' + text
     ref = 'https://github.com/mne-tools/mne-python/' + slug
     set_classes(options)
     node = reference(rawtext, text, refuri=ref, **options)
diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py
--- a/mne/io/edf/edf.py
+++ b/mne/io/edf/edf.py
@@ -538,8 +538,13 @@ def _read_edf_header(fname, exclude):
         hour, minute, sec = [int(x) for x in
                              re.findall(r'(\d+)', fid.read(8).decode())]
         century = 2000 if year < 50 else 1900
-        meas_date = datetime(year + century, month, day, hour, minute, sec,
-                             tzinfo=timezone.utc)
+        try:
+            meas_date = datetime(year + century, month, day, hour, minute, sec,
+                                 tzinfo=timezone.utc)
+        except ValueError:
+            warn(f'Invalid date encountered ({year + century:04d}-{month:02d}-'
+                 f'{day:02d} {hour:02d}:{minute:02d}:{sec:02d}).')
+            meas_date = None
 
         header_nbytes = _edf_str_int(fid.read(8))
 


Test patch:
diff --git a/mne/io/edf/tests/test_edf.py b/mne/io/edf/tests/test_edf.py
--- a/mne/io/edf/tests/test_edf.py
+++ b/mne/io/edf/tests/test_edf.py
@@ -20,7 +20,7 @@
 
 from mne import pick_types, Annotations
 from mne.datasets import testing
-from mne.utils import run_tests_if_main, requires_pandas
+from mne.utils import run_tests_if_main, requires_pandas, _TempDir
 from mne.io import read_raw_edf, read_raw_bdf
 from mne.io.tests.test_raw import _test_raw_reader
 from mne.io.edf.edf import _get_edf_default_event_id
@@ -383,9 +383,6 @@ def test_bdf_multiple_annotation_channels():
     assert_array_equal(descriptions, raw.annotations.description)
 
 
-run_tests_if_main()
-
-
 @testing.requires_testing_data
 def test_edf_lowpass_zero():
     """Test if a lowpass filter of 0Hz is mapped to the Nyquist frequency."""
@@ -399,3 +396,27 @@ def test_edf_annot_sub_s_onset():
     """Test reading of sub-second annotation onsets."""
     raw = read_raw_edf(edf_annot_sub_s_path)
     assert_allclose(raw.annotations.onset, [1.951172, 3.492188])
+
+
+def test_invalid_date():
+    """Test handling of invalid date in EDF header."""
+    tempdir = _TempDir()
+    with open(edf_path, 'rb') as f:  # read valid test file
+        edf = bytearray(f.read())
+    # original date in header is 29.04.14 (2014-04-29) at pos 168:176
+    # create invalid date 29.02.14 (2014 is not a leap year)
+    edf[172] = ord('2')
+    with open(op.join(tempdir, "temp.edf"), "wb") as f:
+        f.write(edf)
+    with pytest.warns(RuntimeWarning, match='Invalid date'):
+        read_raw_edf(op.join(tempdir, "temp.edf"), preload=True)
+
+    # another invalid date 29.00.14 (0 is not a month)
+    edf[172] = ord('0')
+    with open(op.join(tempdir, "temp.edf"), "wb") as f:
+        f.write(edf)
+    with pytest.warns(RuntimeWarning, match='Invalid date'):
+        read_raw_edf(op.join(tempdir, "temp.edf"), preload=True)
+
+
+run_tests_if_main()