Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions neo/rawio/axonrawio.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,24 +710,41 @@ def _parse_abf_v1(f, header_description):
header["sProtocolPath"] = header["sProtocolPath"].replace(b"\\", b"/")

# date and time
# 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.
# lFileStartDate 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; a genuinely out-of-range date is left to
# raise 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)
# lFileStartDate packs the calendar date as decimal digits, but the packing changed across
# ABF1 versions:
# - newer files use YYYYMMDD (4-digit year), e.g. 20050611 -> 2005-06-11
# - older files use YYMMDD (2-digit year), e.g. 180618 -> 2018-06-18
# Detect the old form from the value rather than the version number (whose exact cutoff we
# cannot pin down): a real 4-digit year is >= 1000, so a year field below 100 is a 2-digit
# year. The century is then ambiguous, so we disambiguate with the one hard invariant we
# have: a recording cannot be in the future. Map the 2-digit year to the 2000s, and fall
# back to the 1900s only if that lands past the current year.
date_as_integer = header["lFileStartDate"]
# The digits are laid out as [year][MM][DD], so month and day occupy the low four decimal
# places: dividing by 10_000 discards MMDD and leaves the year.
year = date_as_integer // 10_000
year_is_two_digit = year < 100
if year_is_two_digit:
year += 2000
if year > datetime.datetime.now().year:
year -= 100 # e.g. 98 -> 1998 rather than a not-yet-happened 2098
date_as_integer = year * 10_000 + date_as_integer % 10_000
month = (date_as_integer // 100) % 100 # drop the two DD digits, keep the two MM digits
day = date_as_integer % 100 # the last two digits
hh = int(header["lFileStartTime"] / 3600.0)
mm = int((header["lFileStartTime"] - hh * 3600) / 60)
ss = header["lFileStartTime"] - hh * 3600 - mm * 60
ms = int(np.mod(ss, 1) * 1e6)
ss = int(ss)
header["rec_datetime"] = datetime.datetime(YY, MM, DD, hh, mm, ss, ms)
header["rec_datetime"] = datetime.datetime(year, month, day, hh, mm, ss, ms)

return header

Expand Down
9 changes: 9 additions & 0 deletions neo/test/rawiotest/test_axonrawio.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ def test_v1_reads_real_acquisition_date(self):
# microsecond is a rounding artifact, not a meaningful value to assert.
self.assertEqual(rec_datetime.replace(microsecond=0), expected_datetime)

def test_v1_reads_two_digit_year_date(self):
# Old ABF1 files (before ~v1.65) pack the date as YYMMDD (2-digit year) rather than
# YYYYMMDD. lFileStartDate = 180618 is 2018-06-18, not year 18, so the 2-digit year must be
# expanded to the 2000s.
expected_datetime = datetime.datetime(2018, 6, 18, 17, 34, 27)
header = parse_axon_soup(self.get_local_path("axon/intracellular_data/abf1_episodic_empty_channel_name.abf"))
rec_datetime = header["rec_datetime"]
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,
Expand Down
Loading