diff --git a/neo/rawio/axonrawio.py b/neo/rawio/axonrawio.py index 7351a5008..75457ce57 100644 --- a/neo/rawio/axonrawio.py +++ b/neo/rawio/axonrawio.py @@ -677,15 +677,18 @@ def _parse_abf_v1(f, header_description): header["sProtocolPath"] = header["sProtocolPath"].replace(b"\\", b"/") # date and time - YY = 1900 - MM = 1 - DD = 1 - seconds_per_day = 24 * 3600 - # A "no date" file writes the 0xFFFFFFFF sentinel, which reads as a negative time; fall back to - # rec_datetime=None instead of crashing on the resulting out-of-range time-of-day. - if not (0 <= header["lFileStartTime"] < seconds_per_day): + # lFileStartDate is a YYYYMMDD-packed integer, parsed the same way as uFileStartDate in ABF2. + # A "no date" sentinel means there is no date to build, so fall back to rec_datetime=None. The + # field is signed, so the all-bits-set 0xFFFFFFFF sentinel is read as -1, and 0 is the unset + # value. Any other value is trusted and left to raise if genuinely out of range, so a real + # parsing error surfaces rather than being masked. + no_date_sentinels = (0, -1) + if header["lFileStartDate"] in no_date_sentinels: header["rec_datetime"] = None else: + YY = int(header["lFileStartDate"] / 10000) + MM = int((header["lFileStartDate"] - YY * 10000) / 100) + DD = int(header["lFileStartDate"] - YY * 10000 - MM * 100) hh = int(header["lFileStartTime"] / 3600.0) mm = int((header["lFileStartTime"] - hh * 3600) / 60) ss = header["lFileStartTime"] - hh * 3600 - mm * 60 @@ -1085,6 +1088,7 @@ def safe_decode_units(s): ("lActualAcqLength", 10, "i"), ("nNumPointsIgnored", 14, "h"), ("lActualEpisodes", 16, "i"), + ("lFileStartDate", 20, "i"), ("lFileStartTime", 24, "i"), ("lDataSectionPtr", 40, "i"), ("lTagSectionPtr", 44, "i"), diff --git a/neo/test/rawiotest/test_axonrawio.py b/neo/test/rawiotest/test_axonrawio.py index 50c5ce389..cb1f07a6f 100644 --- a/neo/test/rawiotest/test_axonrawio.py +++ b/neo/test/rawiotest/test_axonrawio.py @@ -1,3 +1,4 @@ +import datetime import unittest from neo.rawio.axonrawio import AxonRawIO, parse_axon_soup @@ -28,6 +29,17 @@ def test_read_raw_protocol(self): reader.read_raw_protocol() + def test_v1_reads_real_acquisition_date(self): + # ABF1 stores the calendar date in lFileStartDate (a YYYYMMDD-packed integer). Older neo + # ignored that field and hardcoded 1900-01-01, so the recording date was always wrong for + # ABF1 files. It must now be read from the header. + expected_datetime = datetime.datetime(2005, 6, 11, 14, 15, 0) + header = parse_axon_soup(self.get_local_path("axon/File_axon_2.abf")) + rec_datetime = header["rec_datetime"] + # Drop sub-second precision: the millisecond field round-trips through a float, so the + # microsecond is a rounding artifact, not a meaningful value to assert. + self.assertEqual(rec_datetime.replace(microsecond=0), expected_datetime) + def test_invalid_date_falls_back_to_none(self): # Some ABF files store an out-of-range / "no date" sentinel (e.g. 0xFFFFFFFF) # in the acquisition date header fields. The date is non-essential annotation, @@ -55,3 +67,4 @@ def test_non_unique_channel_ids_fall_back_to_sequential_ids(self): if __name__ == "__main__": unittest.main() + \ No newline at end of file