Skip to content

Commit 3e23244

Browse files
Merge pull request #784 from G-Node/nixio-datetime-conversions
[NIXIO] Save and load date, time, and datetime annotations
2 parents a9ac68d + e6b6589 commit 3e23244

File tree

2 files changed

+92
-32
lines changed

2 files changed

+92
-32
lines changed

neo/io/nixio.py

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222

2323
from __future__ import absolute_import
2424

25-
import time
26-
from datetime import datetime
25+
from datetime import date, time, datetime
2726
try:
2827
from collections.abc import Iterable
2928
except ImportError:
@@ -55,10 +54,19 @@
5554
except NameError:
5655
string_types = str
5756

57+
datetime_types = (date, time, datetime)
58+
5859
EMPTYANNOTATION = "EMPTYLIST"
5960
ARRAYANNOTATION = "ARRAYANNOTATION"
61+
DATETIMEANNOTATION = "DATETIME"
62+
DATEANNOTATION = "DATE"
63+
TIMEANNOTATION = "TIME"
6064
MIN_NIX_VER = Version("1.5.0")
6165

66+
datefmt = "%Y-%m-%d"
67+
timefmt = "%H:%M:%S.%f"
68+
datetimefmt = datefmt + "T" + timefmt
69+
6270

6371
def stringify(value):
6472
if value is None:
@@ -83,10 +91,40 @@ def units_to_string(pqunit):
8391
return dim
8492

8593

86-
def calculate_timestamp(dt):
94+
def dt_to_nix(dt):
95+
"""
96+
Converts date, time, and datetime objects to an ISO string representation
97+
appropriate for storing in NIX. Returns the converted value and the
98+
annotation type definition for converting back to the original value
99+
type.
100+
"""
87101
if isinstance(dt, datetime):
88-
return int(time.mktime(dt.timetuple()))
89-
return int(dt)
102+
return dt.strftime(datetimefmt), DATETIMEANNOTATION
103+
if isinstance(dt, date):
104+
return dt.strftime(datefmt), DATEANNOTATION
105+
if isinstance(dt, time):
106+
return dt.strftime(timefmt), TIMEANNOTATION
107+
# Unknown: returning as is
108+
return dt
109+
110+
111+
def dt_from_nix(nixdt, annotype):
112+
"""
113+
Inverse function of 'dt_to_nix()'. Requires the stored annotation type to
114+
distinguish between the three source types (date, time, and datetime).
115+
"""
116+
if annotype == DATEANNOTATION:
117+
dt = datetime.strptime(nixdt, datefmt)
118+
return dt.date()
119+
if annotype == TIMEANNOTATION:
120+
dt = datetime.strptime(nixdt, timefmt)
121+
return dt.time()
122+
if annotype == DATETIMEANNOTATION:
123+
dt = datetime.strptime(nixdt, datetimefmt)
124+
return dt
125+
# Unknown type: older (or newer) IO version?
126+
# Returning as is to avoid data loss.
127+
return nixdt
90128

91129

92130
def check_nix_version():
@@ -307,7 +345,6 @@ def _nix_to_neo_segment(self, nix_group):
307345
neo_segment.rec_datetime = datetime.fromtimestamp(
308346
nix_group.created_at
309347
)
310-
311348
self._neo_map[nix_group.name] = neo_segment
312349

313350
# this will probably get all the DAs anyway, but if we change any part
@@ -582,12 +619,12 @@ def write_block(self, block, use_obj_names=False):
582619
metadata["neo_name"] = neoname
583620
nixblock.definition = block.description
584621
if block.rec_datetime:
585-
nixblock.force_created_at(
586-
calculate_timestamp(block.rec_datetime)
587-
)
622+
nix_rec_dt = int(block.rec_datetime.strftime("%s"))
623+
nixblock.force_created_at(nix_rec_dt)
588624
if block.file_datetime:
589-
fdt = calculate_timestamp(block.file_datetime)
590-
metadata["file_datetime"] = fdt
625+
fdt, annotype = dt_to_nix(block.file_datetime)
626+
fdtprop = metadata.create_property("file_datetime", fdt)
627+
fdtprop.definition = annotype
591628
if block.annotations:
592629
for k, v in block.annotations.items():
593630
self._write_property(metadata, k, v)
@@ -683,12 +720,12 @@ def _write_segment(self, segment, nixblock):
683720
metadata["neo_name"] = neoname
684721
nixgroup.definition = segment.description
685722
if segment.rec_datetime:
686-
nixgroup.force_created_at(
687-
calculate_timestamp(segment.rec_datetime)
688-
)
723+
nix_rec_dt = int(segment.rec_datetime.strftime("%s"))
724+
nixgroup.force_created_at(nix_rec_dt)
689725
if segment.file_datetime:
690-
fdt = calculate_timestamp(segment.file_datetime)
691-
metadata["file_datetime"] = fdt
726+
fdt, annotype = dt_to_nix(segment.file_datetime)
727+
fdtprop = metadata.create_property("file_datetime", fdt)
728+
fdtprop.definition = annotype
692729
if segment.annotations:
693730
for k, v in segment.annotations.items():
694731
self._write_property(metadata, k, v)
@@ -1161,8 +1198,10 @@ def _write_property(self, section, name, v):
11611198
else:
11621199
section.create_property(name, v.magnitude.item())
11631200
section.props[name].unit = str(v.dimensionality)
1164-
elif isinstance(v, datetime):
1165-
section.create_property(name, calculate_timestamp(v))
1201+
elif isinstance(v, datetime_types):
1202+
value, annotype = dt_to_nix(v)
1203+
prop = section.create_property(name, value)
1204+
prop.definition = annotype
11661205
elif isinstance(v, string_types):
11671206
if len(v):
11681207
section.create_property(name, v)
@@ -1201,8 +1240,7 @@ def _write_property(self, section, name, v):
12011240
values.append(item)
12021241
section.create_property(name, values)
12031242
section.props[name].unit = unit
1204-
if definition:
1205-
section.props[name].definition = definition
1243+
section.props[name].definition = definition
12061244
elif type(v).__module__ == "numpy":
12071245
section.create_property(name, v.item())
12081246
else:
@@ -1237,6 +1275,9 @@ def _nix_attr_to_neo(nix_obj):
12371275
values = ""
12381276
elif len(values) == 1:
12391277
values = values[0]
1278+
if prop.definition in (DATEANNOTATION, TIMEANNOTATION,
1279+
DATETIMEANNOTATION):
1280+
values = dt_from_nix(values, prop.definition)
12401281
if prop.definition == ARRAYANNOTATION:
12411282
if 'array_annotations' in neo_attrs:
12421283
neo_attrs['array_annotations'][prop.name] = values
@@ -1248,10 +1289,6 @@ def _nix_attr_to_neo(nix_obj):
12481289
# there's no reason to keep it in the annotations
12491290
neo_attrs["name"] = stringify(neo_attrs.pop("neo_name", None))
12501291

1251-
if "file_datetime" in neo_attrs:
1252-
neo_attrs["file_datetime"] = datetime.fromtimestamp(
1253-
neo_attrs["file_datetime"]
1254-
)
12551292
return neo_attrs
12561293

12571294
@staticmethod
@@ -1283,9 +1320,7 @@ def _get_time_dimension(obj):
12831320
return None
12841321

12851322
def _use_obj_names(self, blocks):
1286-
12871323
errmsg = "use_obj_names enabled: found conflict or anonymous object"
1288-
12891324
allobjs = []
12901325

12911326
def check_unique(objs):

neo/test/iotest/test_nixio.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from collections.abc import Iterable
1818
except ImportError:
1919
from collections import Iterable
20-
from datetime import datetime
20+
from datetime import date, time, datetime
2121

2222
from tempfile import mkdtemp
2323

@@ -29,7 +29,8 @@
2929
from neo.core import (Block, Segment, ChannelIndex, AnalogSignal,
3030
IrregularlySampledSignal, Unit, SpikeTrain, Event, Epoch)
3131
from neo.test.iotest.common_io_test import BaseTestIO
32-
from neo.io.nixio import NixIO, create_quantity, units_to_string, neover
32+
from neo.io.nixio import (NixIO, create_quantity, units_to_string, neover,
33+
dt_from_nix, dt_to_nix, DATETIMEANNOTATION)
3334
from neo.io.nixio_fr import NixIO as NixIO_lazy
3435
from neo.io.proxyobjects import AnalogSignalProxy, SpikeTrainProxy, EventProxy, EpochProxy
3536

@@ -319,9 +320,10 @@ def compare_attr(self, neoobj, nixobj):
319320
self.assertEqual(neoobj.rec_datetime,
320321
datetime.fromtimestamp(nixobj.created_at))
321322
if hasattr(neoobj, "file_datetime") and neoobj.file_datetime:
322-
self.assertEqual(neoobj.file_datetime,
323-
datetime.fromtimestamp(
324-
nixobj.metadata["file_datetime"]))
323+
nixdt = dt_from_nix(nixobj.metadata["file_datetime"],
324+
DATETIMEANNOTATION)
325+
assert neoobj.file_datetime == nixdt
326+
self.assertEqual(neoobj.file_datetime, nixdt)
325327
if neoobj.annotations:
326328
nixmd = nixobj.metadata
327329
for k, v, in neoobj.annotations.items():
@@ -1317,7 +1319,7 @@ def test_to_value(self):
13171319
# datetime
13181320
dt = self.rdate()
13191321
writeprop(section, "dt", dt)
1320-
self.assertEqual(datetime.fromtimestamp(section["dt"]), dt)
1322+
self.assertEqual(section["dt"], dt_to_nix(dt)[0])
13211323

13221324
# string
13231325
randstr = self.rsentence()
@@ -1446,6 +1448,29 @@ def generate_complete_block():
14461448

14471449
self.write_and_compare([block_lazy])
14481450

1451+
def test_annotation_types(self):
1452+
annotations = {
1453+
"somedate": self.rdate(),
1454+
"now": datetime.now(),
1455+
"today": date.today(),
1456+
"sometime": time(13, 37, 42),
1457+
"somequantity": self.rquant(10, pq.ms),
1458+
"somestring": self.rsentence(3),
1459+
"npfloat": np.float(10),
1460+
"nparray": np.array([1, 2, 400]),
1461+
"emptystr": "",
1462+
}
1463+
wblock = Block("annotation_block", **annotations)
1464+
self.writer.write_block(wblock)
1465+
rblock = self.writer.read_block(neoname="annotation_block")
1466+
for k in annotations:
1467+
orig = annotations[k]
1468+
readval = rblock.annotations[k]
1469+
if isinstance(orig, np.ndarray):
1470+
np.testing.assert_almost_equal(orig, readval)
1471+
else:
1472+
self.assertEqual(annotations[k], rblock.annotations[k])
1473+
14491474

14501475
@unittest.skipUnless(HAVE_NIX, "Requires NIX")
14511476
class NixIOReadTest(NixIOTest):

0 commit comments

Comments
 (0)